Files
compras/resources/js/pages/company/index.tsx
T
nala 4d42f083ce
tests / ci (push) Canceled after 0s
commit
2026-10-28 12:06:05 +02:00

372 lines
17 KiB
TypeScript

import { Head, useForm } from '@inertiajs/react';
import {
FormErrors,
PageTitle,
Panel,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { money, roleLabels } from '@/types/procurement';
import type { Company, Department, CostCenter } from '@/types/procurement';
type Member = {
id: number;
name: string;
email: string;
department_id: number;
purchasing_role: string;
};
type Props = {
company: Company;
departments: Department[];
costCenters: CostCenter[];
users: Member[];
roles: { value: string; label: string }[];
};
export default function CompanyPage({
company,
departments,
costCenters,
users,
roles,
}: Props) {
const department = useForm({ name: '' });
const center = useForm({ department_id: '', code: '', name: '' });
const user = useForm({
name: '',
email: '',
department_id: '',
purchasing_role: 'requester',
password: '',
password_confirmation: '',
});
return (
<>
<Head title="Empresa e equipa" />
<div className="mx-auto flex w-full max-w-7xl flex-col gap-7 p-5 lg:p-8">
<PageTitle
eyebrow="Administração"
title={company.name}
description="Organize os departamentos, centros de custo e contas da equipa."
/>
<div className="grid gap-5 lg:grid-cols-2">
<Panel
title="Departamentos"
description="Cada chefe aprova apenas pedidos do seu departamento."
>
<div className="mb-5 flex flex-wrap gap-2">
{departments.map((d) => (
<span
className="rounded-md bg-muted px-3 py-1 text-sm"
key={d.id}
>
{d.name}
</span>
))}
</div>
<form
className="space-y-3"
onSubmit={(e) => {
e.preventDefault();
department.post('/empresa/departamentos', {
onSuccess: () => department.reset(),
});
}}
>
<FormErrors errors={department.errors} />
<Label htmlFor="department-name">
Novo departamento
</Label>
<div className="flex gap-2">
<Input
id="department-name"
required
value={department.data.name}
onChange={(e) =>
department.setData(
'name',
e.target.value,
)
}
/>
<Button disabled={department.processing}>
Adicionar
</Button>
</div>
</form>
</Panel>
<Panel title="Centros de custo">
<div className="mb-5 space-y-2 text-sm">
{costCenters.map((c) => (
<p key={c.id}>
<strong>{c.code}</strong> · {c.name}
<span className="ml-2 text-muted-foreground">
{
departments.find(
(d) => d.id === c.department_id,
)?.name
}
</span>
</p>
))}
</div>
<form
className="space-y-3"
onSubmit={(e) => {
e.preventDefault();
center.post('/empresa/centros-de-custo', {
onSuccess: () => center.reset(),
});
}}
>
<FormErrors errors={center.errors} />
<Label htmlFor="center-department">
Departamento
</Label>
<select
id="center-department"
className={controlClass}
required
value={center.data.department_id}
onChange={(e) =>
center.setData(
'department_id',
e.target.value,
)
}
>
<option value="">Seleccione</option>
{departments.map((d) => (
<option key={d.id} value={d.id}>
{d.name}
</option>
))}
</select>
<div className="grid grid-cols-2 gap-3">
{(['code', 'name'] as const).map((key) => (
<div className="space-y-2" key={key}>
<Label htmlFor={`center-${key}`}>
{key === 'code'
? 'Código'
: 'Designação'}
</Label>
<Input
id={`center-${key}`}
required
value={center.data[key]}
onChange={(e) =>
center.setData(
key,
e.target.value,
)
}
/>
</div>
))}
</div>
<Button disabled={center.processing}>
Adicionar centro de custo
</Button>
</form>
</Panel>
</div>
<Panel
title="Equipa"
description="O perfil define as operações disponíveis. Um administrador não substitui os aprovadores."
>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-muted-foreground">
<tr>
<th className="pb-3">Nome</th>
<th>Email</th>
<th>Departamento</th>
<th>Perfil</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr
key={u.id}
className="border-b last:border-0"
>
<td className="py-3 pr-4 font-medium">
{u.name}
</td>
<td className="pr-4">{u.email}</td>
<td className="pr-4">
{
departments.find(
(d) =>
d.id ===
u.department_id,
)?.name
}
</td>
<td>{roleLabels[u.purchasing_role]}</td>
</tr>
))}
</tbody>
</table>
</div>
<form
className="mt-6 space-y-4 border-t pt-5"
autoComplete="off"
onSubmit={(e) => {
e.preventDefault();
user.post('/empresa/utilizadores', {
onSuccess: () => user.reset(),
});
}}
>
<h3 className="text-sm font-semibold">
Adicionar utilizador
</h3>
<FormErrors errors={user.errors} />
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{(
[
{
key: 'name',
label: 'Nome',
type: 'text',
},
{
key: 'email',
label: 'Email',
type: 'email',
},
{
key: 'password',
label: 'Palavra-passe inicial',
type: 'password',
},
{
key: 'password_confirmation',
label: 'Confirmar palavra-passe',
type: 'password',
},
] as const
).map((field) => (
<div className="space-y-2" key={field.key}>
<Label htmlFor={`member-${field.key}`}>
{field.label}
</Label>
<Input
id={`member-${field.key}`}
type={field.type}
autoComplete={
field.type === 'password'
? 'new-password'
: 'off'
}
minLength={
field.type === 'password'
? 12
: undefined
}
required
value={user.data[field.key]}
onChange={(e) =>
user.setData(
field.key,
e.target.value,
)
}
/>
</div>
))}
<div className="space-y-2">
<Label htmlFor="member-department">
Departamento
</Label>
<select
id="member-department"
className={controlClass}
required
value={user.data.department_id}
onChange={(e) =>
user.setData(
'department_id',
e.target.value,
)
}
>
<option value="">Seleccione</option>
{departments.map((d) => (
<option key={d.id} value={d.id}>
{d.name}
</option>
))}
</select>
</div>
<div className="space-y-2">
<Label htmlFor="member-role">Perfil</Label>
<select
id="member-role"
className={controlClass}
value={user.data.purchasing_role}
onChange={(e) =>
user.setData(
'purchasing_role',
e.target.value,
)
}
>
{roles.map((r) => (
<option key={r.value} value={r.value}>
{r.label}
</option>
))}
</select>
</div>
</div>
<p className="text-xs text-muted-foreground">
Palavras-passe com pelo menos 12 caracteres. Nesta
etapa as contas são criadas pelo administrador;
convites por email ficam para uma próxima etapa.
</p>
<Button disabled={user.processing}>
Criar utilizador
</Button>
</form>
</Panel>
<Panel
title="Política inicial de aprovação"
description="Configuração provisória. A alteração com dupla validação administrativa será implementada numa próxima etapa."
>
<div className="grid gap-4 text-sm md:grid-cols-3">
<p>
Até {money(company.first_limit_minor)}
<br />
<strong>Chefe de departamento</strong>
</p>
<p>
Até {money(company.second_limit_minor)}
<br />
<strong>Chefe + Procurement</strong>
</p>
<p>
Acima de {money(company.second_limit_minor)}
<br />
<strong>Chefe + Procurement + Finanças</strong>
</p>
</div>
<p className="mt-4 text-sm text-muted-foreground">
Base de cálculo:{' '}
{company.approval_includes_tax ? 'com' : 'sem'} IVA.
Cada etapa exige uma pessoa distinta do solicitante e
dos aprovadores anteriores.
</p>
</Panel>
</div>
</>
);
}
CompanyPage.layout = {
breadcrumbs: [{ title: 'Empresa e equipa', href: '/empresa' }],
};