294 lines
14 KiB
TypeScript
294 lines
14 KiB
TypeScript
import { useCrmTaskCreation } from '@/lib/crm-task-creation';
|
|
import type { TaskCreationContext } from '@/lib/crm-task-creation';
|
|
import type { Lead, Owner } from '@/pages/crm/types';
|
|
import { Field, button, control } from '@/pages/management/shared';
|
|
import { useState } from 'react';
|
|
|
|
export default function CrmTaskCreation({
|
|
lead,
|
|
owners,
|
|
context,
|
|
}: {
|
|
lead: Lead;
|
|
owners: Owner[];
|
|
context: TaskCreationContext;
|
|
}) {
|
|
const edit = useCrmTaskCreation(lead, context);
|
|
const { form } = edit;
|
|
const [showPreparation, setShowPreparation] = useState(false);
|
|
const preparation = edit.preparation;
|
|
const labels = {
|
|
title: 'Tarefa',
|
|
due_at: 'Prazo',
|
|
assignee_user_id: 'Responsável pela tarefa',
|
|
};
|
|
function display(field: keyof typeof labels, value: string) {
|
|
if (!value) return 'Por preencher';
|
|
if (field === 'assignee_user_id')
|
|
return (
|
|
owners.find((owner) => String(owner.id) === value)?.name ??
|
|
'Responsável anterior indisponível'
|
|
);
|
|
if (field === 'due_at') {
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime())
|
|
? 'Prazo por confirmar'
|
|
: new Intl.DateTimeFormat('pt-MZ', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
timeZone: 'Africa/Maputo',
|
|
}).format(date) + ' (Maputo)';
|
|
}
|
|
return value;
|
|
}
|
|
const missing =
|
|
form.data.assignee_user_id &&
|
|
!owners.some(
|
|
(owner) => String(owner.id) === form.data.assignee_user_id,
|
|
);
|
|
const sentDate = edit.sent ? new Date(edit.sent.due_at) : null;
|
|
const sentDeadline =
|
|
sentDate && !Number.isNaN(sentDate.getTime())
|
|
? new Intl.DateTimeFormat('pt-MZ', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
timeZone: 'Africa/Maputo',
|
|
}).format(sentDate) + ' (Maputo)'
|
|
: 'Prazo por confirmar';
|
|
return (
|
|
<form
|
|
id="new-crm-task"
|
|
className="max-w-xl space-y-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
edit.submit();
|
|
}}
|
|
>
|
|
<h2 className="text-lg font-semibold">Registar próxima acção</h2>
|
|
{context.preparation && (
|
|
<section
|
|
aria-label="Recuperação da preparação"
|
|
className="space-y-3 rounded border p-3"
|
|
>
|
|
<p role="status">
|
|
{preparation.message ||
|
|
(context.preparation.installed
|
|
? 'A preparação acompanha as alterações e pode ser retomada nesta conta.'
|
|
: 'A recuperação persistente ainda não está instalada. A criação manual continua disponível nesta página.')}
|
|
</p>
|
|
{preparation.comparison && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={() => setShowPreparation(true)}
|
|
>
|
|
Comparar preparação guardada
|
|
</button>
|
|
{showPreparation &&
|
|
preparation.comparison.snapshot && (
|
|
<>
|
|
<p>
|
|
Compare antes de adoptar. As
|
|
alterações desta página permanecem
|
|
até confirmar as suas escolhas.
|
|
</p>
|
|
{(
|
|
Object.keys(
|
|
labels,
|
|
) as (keyof typeof labels)[]
|
|
).map((field) => (
|
|
<div
|
|
key={field}
|
|
className="space-y-1"
|
|
>
|
|
<p className="font-semibold">
|
|
{labels[field]}
|
|
</p>
|
|
<p>
|
|
Nesta página:{' '}
|
|
{display(
|
|
field,
|
|
preparation.current
|
|
.values[field],
|
|
)}
|
|
</p>
|
|
<p>
|
|
Guardado:{' '}
|
|
{display(
|
|
field,
|
|
preparation.comparison
|
|
?.snapshot?.values[
|
|
field
|
|
] ?? '',
|
|
)}
|
|
</p>
|
|
<label>
|
|
Valor a retomar:{' '}
|
|
{labels[field]}
|
|
<select
|
|
className={control}
|
|
value={preparation.source(
|
|
field,
|
|
)}
|
|
onChange={(event) =>
|
|
preparation.choose(
|
|
field,
|
|
event.target
|
|
.value ===
|
|
'saved'
|
|
? 'saved'
|
|
: 'page',
|
|
)
|
|
}
|
|
>
|
|
<option value="page">
|
|
Manter desta página
|
|
</option>
|
|
<option value="saved">
|
|
Usar guardado
|
|
</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
))}
|
|
{preparation.comparison.snapshot
|
|
.pending && (
|
|
<p>
|
|
Existe uma tentativa por
|
|
confirmar. A recuperação
|
|
conserva os seus dados e não
|
|
cria a tarefa automaticamente.
|
|
</p>
|
|
)}
|
|
{!!preparation.unresolved.length && (
|
|
<p role="alert">
|
|
Existem alterações posteriores à
|
|
escolha. Reveja esses campos
|
|
antes de retomar.
|
|
</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
disabled={
|
|
!!preparation.unresolved.length
|
|
}
|
|
onClick={preparation.recover}
|
|
>
|
|
Aplicar escolhas e retomar
|
|
</button>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
{preparation.enabled && preparation.state === 'error' && (
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={() => {
|
|
void preparation.ensure(false);
|
|
}}
|
|
>
|
|
Voltar a guardar preparação
|
|
</button>
|
|
)}
|
|
{preparation.enabled &&
|
|
!preparation.comparison &&
|
|
['error', 'unavailable'].includes(
|
|
preparation.state,
|
|
) && (
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
onClick={preparation.manual}
|
|
>
|
|
Continuar só nesta página
|
|
</button>
|
|
)}
|
|
</section>
|
|
)}
|
|
{edit.notice && <p role="status">{edit.notice}</p>}
|
|
{edit.waiting && (
|
|
<p>
|
|
A recuperação repete os dados da tentativa:{' '}
|
|
{edit.sent?.title}; {sentDeadline};{' '}
|
|
{owners.find(
|
|
(owner) =>
|
|
String(owner.id) === edit.sent?.assignee_user_id,
|
|
)?.name ?? 'Responsável do envio anterior'}
|
|
. As edições posteriores ficam conservadas para a próxima
|
|
tarefa.
|
|
</p>
|
|
)}
|
|
<Field label="Tarefa" error={form.errors.title}>
|
|
<input
|
|
className={control}
|
|
maxLength={200}
|
|
value={form.data.title}
|
|
onChange={(e) => edit.change('title', e.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field label="Prazo (hora local)" error={form.errors.due_at}>
|
|
<input
|
|
type="datetime-local"
|
|
className={control}
|
|
value={form.data.due_at}
|
|
onChange={(e) => edit.change('due_at', e.target.value)}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Responsável pela tarefa"
|
|
error={form.errors.assignee_user_id}
|
|
>
|
|
<select
|
|
className={control}
|
|
value={form.data.assignee_user_id}
|
|
onChange={(e) =>
|
|
edit.change('assignee_user_id', e.target.value)
|
|
}
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{missing && (
|
|
<option value={form.data.assignee_user_id}>
|
|
Responsável anterior indisponível — seleccione para
|
|
novo envio
|
|
</option>
|
|
)}
|
|
{owners.map((owner) => (
|
|
<option key={owner.id} value={owner.id}>
|
|
{owner.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
{Object.keys(form.errors).includes('client_key') && (
|
|
<p role="alert">
|
|
Não foi possível reconhecer o envio. Actualize as tarefas
|
|
antes de continuar.
|
|
</p>
|
|
)}
|
|
<div className="flex flex-wrap gap-3">
|
|
<button
|
|
className={button}
|
|
disabled={
|
|
form.processing || edit.blocked || edit.refreshing
|
|
}
|
|
>
|
|
{edit.waiting
|
|
? 'Recuperar a mesma criação'
|
|
: 'Criar tarefa'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
disabled={form.processing || edit.refreshing}
|
|
onClick={edit.refresh}
|
|
>
|
|
Actualizar tarefas
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|