feat: actualizar CDC Brief con fecha limite de entrega
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
Vendored
+1613
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1613
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user