Files
2026-11-03 11:51:44 +02:00

230 lines
9.8 KiB
TypeScript

import { Head, Link, router } from '@inertiajs/react';
import { useState } from 'react';
import { PageTitle, Panel, controlClass } from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Props = {
area: 'procurement' | 'people';
modules: Record<string, string>;
counts: Record<string, number>;
checkedAt: string;
asOf: string;
deadlineCounts: Record<string, number>;
filters: { module: string; search: string; due: string };
tasks: {
total: number;
data: {
id: string;
module: string;
reference: string;
label: string;
blocked: boolean;
created_at: string | null;
due_on: string | null;
due_status: string;
href: string;
}[];
prev_page_url: string | null;
next_page_url: string | null;
};
};
export default function Tasks({
area,
modules,
counts,
checkedAt,
asOf,
deadlineCounts,
filters,
tasks,
}: Props) {
const [search, setSearch] = useState(filters.search);
const [module, setModule] = useState(filters.module);
const [due, setDue] = useState(filters.due);
const deadlines: Record<string, string> = {
overdue: 'Prazo ultrapassado',
today: 'Vence hoje',
upcoming: 'Prazo futuro',
undated: 'Sem prazo registado',
};
const path =
area === 'people' ? '/pessoas/tarefas' : '/requisicoes/tarefas';
return (
<>
<Head title="As minhas tarefas" />
<div className="space-y-6 p-6">
<PageTitle
eyebrow={
area === 'people'
? 'Pessoas e contratações'
: 'Compras, armazém e finanças'
}
title="As minhas tarefas"
description={
area === 'people'
? 'Intervenções atribuídas à sua equipa nos processos de Pessoas, com ligação ao formulário original.'
: 'Requisições, consulta ao mercado, avaliações de propostas, adjudicações, ordens, intervenções de armazém e conferências financeiras, com ligação ao formulário original.'
}
action={
<Button
variant="outline"
onClick={() => router.reload()}
>
Actualizar
</Button>
}
/>
<div className="flex flex-wrap gap-3">
{Object.entries(modules).map(([key, label]) => (
<Link
key={key}
href={`${path}?module=${key}`}
className="rounded-lg border px-4 py-3"
>
{label}: <strong>{counts[key] ?? 0}</strong>
</Link>
))}
</div>
<Panel title="Trabalho pendente">
<form
className="mb-4 flex flex-wrap items-end gap-3"
onSubmit={(event) => {
event.preventDefault();
router.get(
path,
{ module, search, due },
{ preserveState: true },
);
}}
>
<label>
Módulo
<select
className={controlClass}
value={module}
onChange={(event) =>
setModule(event.target.value)
}
>
<option value="all">Todos nesta área</option>
{Object.entries(modules).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
</label>
<label>
Referência ou tarefa
<input
className={controlClass}
value={search}
maxLength={100}
onChange={(event) =>
setSearch(event.target.value)
}
/>
</label>
<label>
Prazo
<select
className={controlClass}
value={due}
onChange={(event) => setDue(event.target.value)}
>
<option value="all">Todos</option>
{Object.entries(deadlines).map(
([value, label]) => (
<option key={value} value={value}>
{label}
</option>
),
)}
</select>
</label>
<Button type="submit" variant="outline">
Filtrar
</Button>
<Link href={path} className="text-sm underline">
Limpar filtros
</Link>
</form>
<p className="mb-4 text-sm text-muted-foreground">
No módulo e pesquisa escolhidos, em {asOf}:{' '}
{Object.entries(deadlines)
.map(
([value, label]) =>
`${label}: ${deadlineCounts[value] ?? 0}`,
)
.join(' · ')}
.
</p>
<p className="mb-4 text-sm text-muted-foreground">
{tasks.total} tarefa(s) no filtro. Documentos mais
antigos aparecem primeiro; esta ordem não define
prioridade de atendimento. Consulta actualizada em{' '}
{new Date(checkedAt).toLocaleString('pt-PT')}.
</p>
{tasks.data.length === 0 ? (
<p>
Sem intervenções pendentes neste âmbito. A consulta
administrativa, por si só, não atribui tarefas
operacionais.
</p>
) : (
<div className="space-y-3">
{tasks.data.map((task) => (
<article
key={task.id}
className="rounded-lg border p-4"
>
<p className="text-xs text-muted-foreground">
{modules[task.module]} ·{' '}
{task.reference}
</p>
<Link
href={task.href}
className="font-medium underline"
>
{task.label}
</Link>
{task.blocked && (
<p className="mt-2 text-sm">
A entrega aguarda conferência do
stock, qualidade ou reserva. Abra o
pedido para consultar o impedimento
e os responsáveis elegíveis.
</p>
)}
{task.due_on && (
<p className="mt-2 text-sm">
Prazo registado no processo:{' '}
{task.due_on}
{' · '}
{deadlines[task.due_status]}
</p>
)}
</article>
))}
</div>
)}
<div className="mt-4 flex gap-4">
{tasks.prev_page_url && (
<Link href={tasks.prev_page_url}>Anterior</Link>
)}
{tasks.next_page_url && (
<Link href={tasks.next_page_url}>Seguinte</Link>
)}
</div>
</Panel>
<p className="text-sm text-muted-foreground">
Abrir uma tarefa não decide nem conclui o processo. A lista
muda quando a intervenção é registada na origem ou quando
mudam as suas competências.
</p>
</div>
</>
);
}