230 lines
9.8 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|