Files
compras/resources/js/pages/people/access.tsx
T
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

183 lines
6.6 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import {
FormErrors,
PageTitle,
Panel,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Assignment = {
user_id: number;
scopes: string[];
expires_at: string | null;
};
function Person({
person,
scopes,
assigned,
version,
processId,
}: {
person: { id: number; name: string };
scopes: Record<string, string>;
assigned?: Assignment;
version: number;
processId: number;
}) {
const expiry = assigned?.expires_at
? new Date(assigned.expires_at.replace(' ', 'T') + 'Z')
: null;
const localExpiry = expiry
? new Date(expiry.getTime() - expiry.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16)
: '';
const form = useForm({
version,
user_id: person.id,
scopes: assigned?.scopes ?? [],
expires_at: localExpiry,
reason: '',
});
return (
<details className="rounded-lg border p-4">
<summary className="cursor-pointer font-medium">
{person.name} · {assigned?.scopes.length ?? 0} atribuição(ões)
</summary>
<form
className="mt-4 space-y-4"
onSubmit={(e) => {
e.preventDefault();
form.transform((data) => ({
...data,
expires_at: data.expires_at
? new Date(data.expires_at).toISOString()
: null,
}));
form.post(`/pessoas/${processId}/acessos`, {
preserveState: 'errors',
preserveScroll: true,
});
}}
>
<FormErrors errors={form.errors} />
<div className="grid gap-3 md:grid-cols-2">
{Object.entries(scopes).map(([key, label]) => (
<label className="flex gap-3" key={key}>
<input
type="checkbox"
checked={form.data.scopes.includes(key)}
onChange={(e) =>
form.setData(
'scopes',
e.target.checked
? [...form.data.scopes, key]
: form.data.scopes.filter(
(s) => s !== key,
),
)
}
/>
{label}
</label>
))}
</div>
<label className="block">
Validade até (opcional)
<input
className={controlClass}
type="datetime-local"
value={form.data.expires_at}
onChange={(e) =>
form.setData('expires_at', e.target.value)
}
/>
</label>
<label className="block">
Motivo
<textarea
className={controlClass}
required
minLength={10}
maxLength={4000}
value={form.data.reason}
onChange={(e) => form.setData('reason', e.target.value)}
/>
</label>
<p className="text-sm">
Retirar todas as opções revoga o acesso. Seleccionar uma
intervenção inclui a consulta do processo; dados
financeiros, entrevistas e minuta têm permissões próprias. A
atribuição não elimina as regras de independência.
</p>
<Button disabled={form.processing}>Guardar atribuição</Button>
</form>
</details>
);
}
export default function Access({
process,
version,
scopes,
users,
assignments,
canView,
}: {
process: { id: number; kind: string; status: string };
version: number;
scopes: Record<string, string>;
users: { id: number; name: string }[];
assignments: Assignment[];
canView: boolean;
}) {
return (
<>
<Head title="Equipa do processo" />
<div className="mx-auto max-w-5xl space-y-6 p-5 lg:p-8">
<PageTitle
eyebrow={
process.kind === 'staff'
? 'Recrutamento'
: 'Serviços individuais'
}
title={`Equipa do processo #${process.id}`}
description="Configurar a equipa não concede acesso automático a candidaturas, entrevistas ou condições financeiras."
/>
<Link href="/pessoas" className="underline">
Voltar a Pessoas e contratações
</Link>
{canView && (
<Link
href={`/pessoas/${process.id}`}
className="ml-4 underline"
>
Consultar processo
</Link>
)}
<Panel title="Atribuições nominativas">
<p className="mb-4 text-sm">
Escolha pessoas com perfil compatível na parametrização
da empresa. Separe preparação, avaliação e decisão.
Versão {version}.
</p>
<div className="space-y-3">
{users.map((person) => (
<Person
key={`${person.id}-${version}`}
person={person}
scopes={scopes}
assigned={assignments.find(
(a) => a.user_id === person.id,
)}
version={version}
processId={process.id}
/>
))}
</div>
</Panel>
</div>
</>
);
}