372 lines
17 KiB
TypeScript
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' }],
|
|
};
|