feat: actualizar CDC Brief con fecha limite de entrega

This commit is contained in:
Isaac_Aracena
2026-05-25 21:20:48 -04:00
parent d7094c94cf
commit 82d59a3313
8 changed files with 1725 additions and 1624 deletions
+7
View File
@@ -54,3 +54,10 @@ El proyecto requiere las siguientes variables en un archivo `.env`:
## Nota de Seguridad ## Nota de Seguridad
**IMPORTANTE:** Nunca subas el archivo `.env` o cualquier archivo con credenciales reales al repositorio. El archivo `.gitignore` está configurado para excluir archivos sensibles. Solo comparte `.env.example` con los nombres de las variables necesarias. **IMPORTANTE:** Nunca subas el archivo `.env` o cualquier archivo con credenciales reales al repositorio. El archivo `.gitignore` está configurado para excluir archivos sensibles. Solo comparte `.env.example` con los nombres de las variables necesarias.
## Cambio reciente
- Se agregó el campo obligatorio **Fecha límite de entrega** en el paso final de revisión.
- El frontend envía la fecha al webhook como `deliveryDate` y `fechaEntrega`.
- La fecha también aparece en el resumen enviado al portal para que n8n pueda usarla en el brief, correo y Sheet.
+1613
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1613
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CDC Brief GLM</title> <title>CDC Brief GLM</title>
<script type="module" crossorigin src="/brief/assets/index-C_JoOsGT.js"></script> <script type="module" crossorigin src="/brief/assets/index-BjRIMN2B.js"></script>
<link rel="stylesheet" crossorigin href="/brief/assets/index-C16767E5.css"> <link rel="stylesheet" crossorigin href="/brief/assets/index-D-Qs_b2T.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+12 -1
View File
@@ -75,7 +75,9 @@ export default function App() {
const handleSendMessage = async ( const handleSendMessage = async (
text: string, text: string,
attachments: Attachment[], attachments: Attachment[],
audio?: AudioRecording audio?: AudioRecording,
deliveryDate?: string,
deliveryDateStatus: 'defined' | 'pending' = 'defined'
) => { ) => {
const userMessageId = Math.random().toString(36).substring(7); const userMessageId = Math.random().toString(36).substring(7);
@@ -109,6 +111,15 @@ export default function App() {
formData.append('submittedByEmail', submittedByEmail); formData.append('submittedByEmail', submittedByEmail);
formData.append('submittedByUid', user?.uid || ''); formData.append('submittedByUid', user?.uid || '');
formData.append('submittedByPhotoUrl', user?.photoURL || ''); formData.append('submittedByPhotoUrl', user?.photoURL || '');
const deliveryDateValue = deliveryDateStatus === 'pending' ? '' : deliveryDate || '';
const deliveryDateLabel = deliveryDateStatus === 'pending' ? 'Por confirmar' : deliveryDateValue;
formData.append('deliveryDate', deliveryDateValue);
formData.append('fechaEntrega', deliveryDateValue);
formData.append('deliveryDateStatus', deliveryDateStatus);
formData.append('fechaEntregaEstado', deliveryDateStatus === 'pending' ? 'por_confirmar' : 'definida');
formData.append('deliveryDateLabel', deliveryDateLabel);
formData.append('fechaEntregaTexto', deliveryDateLabel);
let generalFileIndex = 0; let generalFileIndex = 0;
let campaignFileIndex = 0; let campaignFileIndex = 0;
+90 -7
View File
@@ -21,7 +21,7 @@ import { Attachment, AttachmentCategory, AttachmentType, AudioRecording } from '
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
interface InputAreaProps { interface InputAreaProps {
onSendMessage: (text: string, attachments: Attachment[], audio?: AudioRecording) => void; onSendMessage: (text: string, attachments: Attachment[], audio?: AudioRecording, deliveryDate?: string, deliveryDateStatus?: 'defined' | 'pending') => void;
isProcessing: boolean; isProcessing: boolean;
} }
@@ -88,6 +88,20 @@ const formatDuration = (seconds: number) => {
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}; };
const formatDeliveryDate = (dateString: string) => {
if (!dateString) return 'Pendiente';
const [year, month, day] = dateString.split('-').map(Number);
if (!year || !month || !day) return dateString;
const date = new Date(year, month - 1, day);
return date.toLocaleDateString('es-DO', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
};
export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessing }) => { export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessing }) => {
const [text, setText] = useState(''); const [text, setText] = useState('');
const [attachments, setAttachments] = useState<Attachment[]>([]); const [attachments, setAttachments] = useState<Attachment[]>([]);
@@ -97,6 +111,8 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
const [currentStep, setCurrentStep] = useState<GuidedStep>('audio'); const [currentStep, setCurrentStep] = useState<GuidedStep>('audio');
const [hasCampaignAssets, setHasCampaignAssets] = useState<CampaignAnswer>(null); const [hasCampaignAssets, setHasCampaignAssets] = useState<CampaignAnswer>(null);
const [validationMessage, setValidationMessage] = useState<string | null>(null); const [validationMessage, setValidationMessage] = useState<string | null>(null);
const [deliveryDate, setDeliveryDate] = useState('');
const [deliveryDatePending, setDeliveryDatePending] = useState(false);
const [audioPreviewPlaying, setAudioPreviewPlaying] = useState(false); const [audioPreviewPlaying, setAudioPreviewPlaying] = useState(false);
const [audioPreviewCurrentTime, setAudioPreviewCurrentTime] = useState(0); const [audioPreviewCurrentTime, setAudioPreviewCurrentTime] = useState(0);
@@ -132,7 +148,9 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
const requiredCompleted = Boolean(audioRecording) && imageAttachments.length > 0; const requiredCompleted = Boolean(audioRecording) && imageAttachments.length > 0;
const campaignStepCompleted = hasCampaignAssets !== 'yes' || campaignAttachments.length > 0; const campaignStepCompleted = hasCampaignAssets !== 'yes' || campaignAttachments.length > 0;
const canSend = requiredCompleted && campaignStepCompleted; const hasDeliveryInfo = Boolean(deliveryDate) || deliveryDatePending;
const deliveryDateLabel = deliveryDatePending ? 'Por confirmar' : formatDeliveryDate(deliveryDate);
const canSend = requiredCompleted && campaignStepCompleted && hasDeliveryInfo;
const progressValue = ((STEP_DEFINITIONS.findIndex((step) => step.id === currentStep) + 1) / STEP_DEFINITIONS.length) * 100; const progressValue = ((STEP_DEFINITIONS.findIndex((step) => step.id === currentStep) + 1) / STEP_DEFINITIONS.length) * 100;
useEffect(() => { useEffect(() => {
@@ -396,7 +414,8 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
`Nota de voz: ${audioRecording ? 'sí' : 'no'}`, `Nota de voz: ${audioRecording ? 'sí' : 'no'}`,
`Imágenes de referencia: ${imageAttachments.length}`, `Imágenes de referencia: ${imageAttachments.length}`,
`Documentos relacionados: ${documentAttachments.length}`, `Documentos relacionados: ${documentAttachments.length}`,
`Campaña o elementos gráficos existentes: ${hasCampaignAssets === 'yes' ? 'sí' : hasCampaignAssets === 'no' ? 'no' : 'no indicado'}`, `Fecha límite de entrega: ${deliveryDateLabel}`,
`Campaña o elementos gráficos existentes: ${hasCampaignAssets === 'yes' ? 'sí' : hasCampaignAssets === 'no' ? 'no' : 'No indicado'}`,
`Archivos de campaña / elementos gráficos: ${campaignAttachments.length}`, `Archivos de campaña / elementos gráficos: ${campaignAttachments.length}`,
]; ];
@@ -424,8 +443,19 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
return; return;
} }
if (!hasDeliveryInfo) {
setValidationMessage('Selecciona la fecha límite de entrega o marca la opción Por confirmar antes de enviar.');
return;
}
if (!isProcessing) { if (!isProcessing) {
onSendMessage(buildSubmissionText(), attachments, audioRecording || undefined); onSendMessage(
buildSubmissionText(),
attachments,
audioRecording || undefined,
deliveryDatePending ? '' : deliveryDate,
deliveryDatePending ? 'pending' : 'defined'
);
setText(''); setText('');
setAttachments([]); setAttachments([]);
if (audioPreviewRef.current) { if (audioPreviewRef.current) {
@@ -438,6 +468,8 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
recordingDurationRef.current = 0; recordingDurationRef.current = 0;
setRecordingDuration(0); setRecordingDuration(0);
setHasCampaignAssets(null); setHasCampaignAssets(null);
setDeliveryDate('');
setDeliveryDatePending(false);
setCurrentStep('audio'); setCurrentStep('audio');
setValidationMessage(null); setValidationMessage(null);
} }
@@ -789,19 +821,64 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
<p className="mb-5 text-sm leading-7 text-zinc-300"> <p className="mb-5 text-sm leading-7 text-zinc-300">
Revisa el resumen antes de enviar. Este es el único paso donde se puede finalizar la solicitud. Revisa el resumen antes de enviar. Este es el único paso donde se puede finalizar la solicitud.
</p> </p>
<div className="mb-4 rounded-2xl border border-indigo-400/20 bg-indigo-500/10 p-4">
<label htmlFor="delivery-date" className="flex flex-col gap-2">
<span className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-200">
Fecha límite de entrega
</span>
<span className="text-sm leading-6 text-zinc-300">
Indica para cuándo debe entregarse esta solicitud. Este dato se enviará al flujo, al correo y al brief generado.
</span>
<input
id="delivery-date"
type="date"
value={deliveryDate}
onChange={(event) => {
setDeliveryDate(event.target.value);
if (event.target.value) {
setDeliveryDatePending(false);
setValidationMessage(null);
}
}}
disabled={isProcessing || deliveryDatePending}
className="mt-2 w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-3 text-sm font-semibold text-white outline-none transition focus:border-indigo-300/60 disabled:opacity-60 sm:max-w-xs"
/>
<label className="mt-3 flex w-fit cursor-pointer items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-zinc-200 transition hover:border-white/20 hover:bg-black/30">
<input
type="checkbox"
checked={deliveryDatePending}
onChange={(event) => {
const checked = event.target.checked;
setDeliveryDatePending(checked);
if (checked) {
setDeliveryDate('');
setValidationMessage(null);
}
}}
disabled={isProcessing}
className="h-4 w-4 rounded border-white/20 bg-black/40 text-indigo-500 focus:ring-indigo-400"
/>
<span>Por confirmar</span>
</label>
</label>
</div>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-black/20 p-4"> <div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<p className="text-xs uppercase tracking-[0.22em] text-zinc-500">Obligatorios</p> <p className="text-xs uppercase tracking-[0.22em] text-zinc-500">Obligatorios</p>
<div className="mt-3 space-y-2 text-sm text-zinc-300"> <div className="mt-3 space-y-2 text-sm text-zinc-300">
<p>Nota de voz: <span className={audioRecording ? 'text-emerald-300' : 'text-amber-300'}>{audioRecording ? 'lista' : 'pendiente'}</span></p> <p>Nota de voz: <span className={audioRecording ? 'text-emerald-300' : 'text-amber-300'}>{audioRecording ? 'Lista' : 'Pendiente'}</span></p>
<p>Imágenes: <span className={imageAttachments.length ? 'text-emerald-300' : 'text-amber-300'}>{imageAttachments.length}</span></p> <p>Imágenes: <span className={imageAttachments.length ? 'text-emerald-300' : 'text-amber-300'}>{imageAttachments.length}</span></p>
<p>Fecha de entrega: <span className={hasDeliveryInfo ? 'text-emerald-300' : 'text-amber-300'}>{hasDeliveryInfo ? deliveryDateLabel : 'Pendiente'}</span></p>
</div> </div>
</div> </div>
<div className="rounded-2xl border border-white/10 bg-black/20 p-4"> <div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<p className="text-xs uppercase tracking-[0.22em] text-zinc-500">Soporte</p> <p className="text-xs uppercase tracking-[0.22em] text-zinc-500">Soporte</p>
<div className="mt-3 space-y-2 text-sm text-zinc-300"> <div className="mt-3 space-y-2 text-sm text-zinc-300">
<p>Documentos: <span className="text-white">{documentAttachments.length}</span></p> <p>Documentos: <span className="text-white">{documentAttachments.length}</span></p>
<p>Campaña / gráficos: <span className="text-white">{hasCampaignAssets === 'yes' ? `${campaignAttachments.length} archivo(s)` : hasCampaignAssets === 'no' ? 'no aplica' : 'no indicado'}</span></p> <p>Campaña / gráficos: <span className="text-white">{hasCampaignAssets === 'yes' ? `${campaignAttachments.length} archivo(s)` : hasCampaignAssets === 'no' ? 'no aplica' : 'No indicado'}</span></p>
<p>Enlaces: <span className="text-white">{linksCount}</span></p> <p>Enlaces: <span className="text-white">{linksCount}</span></p>
</div> </div>
</div> </div>
@@ -829,7 +906,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
<div className="text-sm text-zinc-400"> <div className="text-sm text-zinc-400">
{canSend {canSend
? 'Todo está listo. El envío solo se realiza desde el paso final.' ? 'Todo está listo. El envío solo se realiza desde el paso final.'
: 'Completa primero la nota de voz y las imágenes. Si marcaste campaña, adjunta sus archivos.'} : 'Completa la nota de voz, las imágenes y la fecha límite de entrega o marca Por confirmar. Si marcaste campaña, adjunta sus archivos.'}
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@@ -875,6 +952,12 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
{imageAttachments.length > 0 ? `${imageAttachments.length} cargada(s)` : 'Pendiente'} {imageAttachments.length > 0 ? `${imageAttachments.length} cargada(s)` : 'Pendiente'}
</span> </span>
</div> </div>
<div className="flex items-center justify-between gap-3 text-sm">
<span className="text-zinc-300">Fecha de entrega</span>
<span className={cn('text-right font-semibold', hasDeliveryInfo ? 'text-emerald-300' : 'text-zinc-500')}>
{hasDeliveryInfo ? deliveryDateLabel : 'Pendiente'}
</span>
</div>
</div> </div>
</div> </div>