Files
Investidor-V2/resources/js/components/crm-task-creation.tsx
T

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>
);
}