1017 lines
51 KiB
TypeScript
1017 lines
51 KiB
TypeScript
import { Head, Link, useForm } from '@inertiajs/react';
|
|
import { AiPlanSettings } from '@/components/ai-plan-settings';
|
|
import type { AiPlan } from '@/components/ai-plan-settings';
|
|
import { IntelligenceSettings } from '@/components/intelligence-settings';
|
|
import type { IntelligencePolicy } from '@/components/intelligence-settings';
|
|
import { comparisonCriteria } from '@/components/comparison-policy-summary';
|
|
import type { ComparisonPolicy } from '@/components/comparison-policy-summary';
|
|
import {
|
|
PageTitle,
|
|
Panel,
|
|
FormErrors,
|
|
controlClass,
|
|
} from '@/components/procurement';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
type Profile = { id: number; name: string; permissions: string[] };
|
|
type Member = {
|
|
user_id: number;
|
|
profile_id: number;
|
|
maintenance: boolean;
|
|
expires_at: string | null;
|
|
reason: string | null;
|
|
};
|
|
type Person = {
|
|
id: number;
|
|
name: string;
|
|
email: string;
|
|
department_id: number;
|
|
};
|
|
const base = '/administracao/parametrizacao';
|
|
function Choices({
|
|
profiles,
|
|
selected,
|
|
change,
|
|
}: {
|
|
profiles: Profile[];
|
|
selected: number[];
|
|
change: (ids: number[]) => void;
|
|
}) {
|
|
return (
|
|
<div className="flex flex-wrap gap-4">
|
|
{profiles.map((p) => (
|
|
<label className="flex items-center gap-2 text-sm" key={p.id}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.includes(p.id)}
|
|
onChange={(e) =>
|
|
change(
|
|
e.target.checked
|
|
? [...selected, p.id]
|
|
: selected.filter((id) => id !== p.id),
|
|
)
|
|
}
|
|
/>
|
|
{p.name}
|
|
</label>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
function Assignment({
|
|
person,
|
|
profiles,
|
|
members,
|
|
version,
|
|
}: {
|
|
person: Person;
|
|
profiles: Profile[];
|
|
members: Member[];
|
|
version: number;
|
|
}) {
|
|
const assigned = members.filter((m) => m.user_id === person.id);
|
|
const form = useForm({
|
|
version,
|
|
user_id: person.id,
|
|
profile_ids: assigned.map((m) => m.profile_id),
|
|
maintenance: assigned.some((m) => m.maintenance),
|
|
expires_at: '',
|
|
reason: '',
|
|
});
|
|
return (
|
|
<details className="rounded border p-4">
|
|
<summary className="cursor-pointer font-medium">
|
|
{person.name} · {person.email} · {assigned.length} perfil(is)
|
|
</summary>
|
|
<form
|
|
className="mt-4 space-y-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.transform((values) => ({
|
|
...values,
|
|
expires_at: values.expires_at
|
|
? new Date(values.expires_at).toISOString()
|
|
: null,
|
|
}));
|
|
form.post(base + '/assign', {
|
|
preserveState: 'errors',
|
|
preserveScroll: true,
|
|
});
|
|
}}
|
|
>
|
|
<FormErrors errors={form.errors} />
|
|
<Choices
|
|
profiles={profiles}
|
|
selected={form.data.profile_ids}
|
|
change={(ids) => form.setData('profile_ids', ids)}
|
|
/>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={form.data.maintenance}
|
|
onChange={(e) =>
|
|
form.setData('maintenance', e.target.checked)
|
|
}
|
|
/>
|
|
Manutenção TEMPO — diagnóstico e consulta temporários
|
|
</label>
|
|
<label className="block space-y-1">
|
|
Expiração (obrigatória para manutenção)
|
|
<input
|
|
className={controlClass}
|
|
type="datetime-local"
|
|
required={form.data.maintenance}
|
|
value={form.data.expires_at}
|
|
onChange={(e) =>
|
|
form.setData('expires_at', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
{assigned.some((m) => m.expires_at) && (
|
|
<p className="text-sm">
|
|
Expiração anterior:{' '}
|
|
{assigned.find((m) => m.expires_at)?.expires_at}.
|
|
Indique a expiração para manter um acesso temporário.
|
|
</p>
|
|
)}
|
|
<label className="block space-y-1">
|
|
Fundamento da atribuição ou revogação
|
|
<textarea
|
|
required
|
|
minLength={10}
|
|
maxLength={4000}
|
|
className={controlClass}
|
|
value={form.data.reason}
|
|
onChange={(e) => form.setData('reason', e.target.value)}
|
|
/>
|
|
</label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Sem perfis seleccionados, o acesso funcional será revogado
|
|
quando a parametrização estiver activa. Os dois
|
|
administradores permanentes devem ser mantidos.
|
|
</p>
|
|
<Button disabled={form.processing}>Guardar atribuição</Button>
|
|
</form>
|
|
</details>
|
|
);
|
|
}
|
|
export default function Parameters({
|
|
ready,
|
|
active,
|
|
version,
|
|
permissions,
|
|
profiles,
|
|
memberships,
|
|
users,
|
|
departments,
|
|
costCenters,
|
|
notice,
|
|
companyRules,
|
|
comparisonPolicy,
|
|
intelligencePolicy,
|
|
intelligenceReady = false,
|
|
aiPlan,
|
|
aiDomains = {},
|
|
aiDataScopes = {},
|
|
}: {
|
|
ready: boolean;
|
|
comparisonPolicy: ComparisonPolicy;
|
|
intelligencePolicy?: IntelligencePolicy;
|
|
intelligenceReady?: boolean;
|
|
aiPlan?: AiPlan;
|
|
aiDomains?: Record<string, string>;
|
|
aiDataScopes?: Record<string, string>;
|
|
active: boolean;
|
|
version: number;
|
|
permissions: { [key: string]: string };
|
|
profiles: Profile[];
|
|
memberships: Member[];
|
|
users: Person[];
|
|
departments: { id: number; name: string }[];
|
|
costCenters: { id: number; code: string; name: string }[];
|
|
notice?: string;
|
|
companyRules: {
|
|
first_limit: string;
|
|
second_limit: string;
|
|
includes_tax: boolean;
|
|
provisional: boolean;
|
|
default_tax_rate: string;
|
|
};
|
|
}) {
|
|
const profile = useForm({
|
|
version,
|
|
id: '',
|
|
name: '',
|
|
permissions: [] as string[],
|
|
});
|
|
const user = useForm({
|
|
version,
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
password_confirmation: '',
|
|
department_id: '',
|
|
profile_ids: [] as number[],
|
|
reason: '',
|
|
});
|
|
const activate = useForm({ version, confirm: false, reason: '' });
|
|
const rules = useForm({
|
|
version,
|
|
first_limit: companyRules.first_limit,
|
|
second_limit: companyRules.second_limit,
|
|
includes_tax: companyRules.includes_tax,
|
|
reason: '',
|
|
});
|
|
const department = useForm({ name: '' });
|
|
const tax = useForm({
|
|
version,
|
|
default_tax_rate: companyRules.default_tax_rate,
|
|
reason: '',
|
|
});
|
|
const center = useForm({ department_id: '', code: '', name: '' });
|
|
const comparison = useForm({
|
|
version,
|
|
minimum_proposals: comparisonPolicy.minimum_proposals,
|
|
minimum_evaluators: comparisonPolicy.minimum_evaluators,
|
|
weights: { ...comparisonPolicy.weights },
|
|
reason: '',
|
|
});
|
|
return (
|
|
<>
|
|
<Head title="Parametrização da aplicação" />
|
|
<Link
|
|
href="/administracao/candidaturas/formularios"
|
|
className="m-5 inline-block font-semibold underline"
|
|
>
|
|
Formulários de inscrições e candidaturas
|
|
</Link>
|
|
<div className="mx-auto max-w-6xl space-y-6 px-5 py-8 lg:px-8">
|
|
<PageTitle
|
|
eyebrow="Administração"
|
|
title="Parametrização da aplicação"
|
|
description="Defina os perfis da empresa, atribua responsabilidades e mantenha dois administradores permanentes."
|
|
/>
|
|
{notice && (
|
|
<p
|
|
role="status"
|
|
className="rounded border border-teal-300 p-3"
|
|
>
|
|
{notice}
|
|
</p>
|
|
)}
|
|
{!ready ? (
|
|
<Panel title="Instalação pendente">
|
|
<p>
|
|
A parametrização ainda não foi instalada neste
|
|
ambiente.
|
|
</p>
|
|
</Panel>
|
|
) : (
|
|
<>
|
|
<Panel
|
|
title={
|
|
active
|
|
? 'Parametrização activa'
|
|
: 'Preparar a configuração da empresa'
|
|
}
|
|
>
|
|
<p className="text-sm">
|
|
{active
|
|
? 'Os acessos são determinados pelos perfis e atribuições abaixo.'
|
|
: 'Crie os perfis e associe os utilizadores antes de activar. A activação substitui os perfis anteriores; utilizadores sem atribuições válidas deixam de ter acesso funcional.'}{' '}
|
|
Versão {version}.
|
|
</p>
|
|
{!active && (
|
|
<form
|
|
className="mt-4 space-y-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
activate.post(base + '/activate', {
|
|
preserveState: 'errors',
|
|
preserveScroll: true,
|
|
});
|
|
}}
|
|
>
|
|
<FormErrors errors={activate.errors} />
|
|
<label className="block">
|
|
Motivo da activação
|
|
<textarea
|
|
required
|
|
minLength={10}
|
|
className={controlClass}
|
|
value={activate.data.reason}
|
|
onChange={(e) =>
|
|
activate.setData(
|
|
'reason',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
required
|
|
type="checkbox"
|
|
checked={activate.data.confirm}
|
|
onChange={(e) =>
|
|
activate.setData(
|
|
'confirm',
|
|
e.target.checked,
|
|
)
|
|
}
|
|
/>
|
|
Conferi os dois administradores e o
|
|
efeito das atribuições sobre todos os
|
|
utilizadores.
|
|
</label>
|
|
<Button
|
|
disabled={
|
|
activate.processing ||
|
|
!activate.data.confirm
|
|
}
|
|
>
|
|
Activar configuração da empresa
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</Panel>
|
|
{intelligencePolicy && (
|
|
<IntelligenceSettings
|
|
key={`intelligence-${version}`}
|
|
policy={intelligencePolicy}
|
|
version={version}
|
|
ready={intelligenceReady}
|
|
/>
|
|
)}
|
|
{aiPlan && (
|
|
<AiPlanSettings
|
|
key={`ai-plan-${version}`}
|
|
plan={aiPlan}
|
|
domains={aiDomains}
|
|
scopes={aiDataScopes}
|
|
version={version}
|
|
ready={intelligenceReady}
|
|
/>
|
|
)}
|
|
<Panel title="Regras de comparação de propostas">
|
|
<p className="mb-4 text-sm">
|
|
{comparisonPolicy.configured
|
|
? `Revisão ${comparisonPolicy.revision} da empresa.`
|
|
: 'Valores iniciais de compatibilidade.'}{' '}
|
|
A alteração aplica-se apenas a requisições
|
|
criadas depois de guardar. Rascunhos e processos
|
|
existentes conservam a regra anterior. Os
|
|
avaliadores devem continuar independentes e sem
|
|
conflito de interesses.
|
|
</p>
|
|
<form
|
|
className="space-y-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
comparison.post(base + '/comparison', {
|
|
preserveState: 'errors',
|
|
preserveScroll: true,
|
|
});
|
|
}}
|
|
>
|
|
<FormErrors errors={comparison.errors} />
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<label className="block space-y-1">
|
|
Mínimo de fornecedores com proposta
|
|
elegível
|
|
<input
|
|
className={controlClass}
|
|
type="number"
|
|
min="1"
|
|
max="100"
|
|
step="1"
|
|
required
|
|
value={
|
|
comparison.data
|
|
.minimum_proposals
|
|
}
|
|
onChange={(e) =>
|
|
comparison.setData(
|
|
'minimum_proposals',
|
|
Number(e.target.value),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="block space-y-1">
|
|
Mínimo de avaliadores independentes por
|
|
proposta
|
|
<input
|
|
className={controlClass}
|
|
type="number"
|
|
min="1"
|
|
max="100"
|
|
step="1"
|
|
required
|
|
value={
|
|
comparison.data
|
|
.minimum_evaluators
|
|
}
|
|
onChange={(e) =>
|
|
comparison.setData(
|
|
'minimum_evaluators',
|
|
Number(e.target.value),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<fieldset className="grid gap-4 md:grid-cols-3">
|
|
<legend className="mb-3 font-medium">
|
|
Ponderações — soma obrigatória de 100%
|
|
</legend>
|
|
{Object.entries(comparisonCriteria).map(
|
|
([key, label]) => (
|
|
<label
|
|
key={key}
|
|
className="block space-y-1"
|
|
>
|
|
{label} (%)
|
|
<input
|
|
className={controlClass}
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
step="1"
|
|
required
|
|
value={
|
|
comparison.data.weights[
|
|
key as keyof typeof comparisonCriteria
|
|
]
|
|
}
|
|
onChange={(e) =>
|
|
comparison.setData(
|
|
'weights',
|
|
{
|
|
...comparison
|
|
.data
|
|
.weights,
|
|
[key]: Number(
|
|
e.target
|
|
.value,
|
|
),
|
|
},
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
),
|
|
)}
|
|
</fieldset>
|
|
<p className="text-sm">
|
|
Soma actual:{' '}
|
|
{Object.values(
|
|
comparison.data.weights,
|
|
).reduce((sum, weight) => sum + weight, 0)}
|
|
%.
|
|
</p>
|
|
<label className="block space-y-1">
|
|
Fundamento e âmbito da alteração
|
|
<textarea
|
|
className={controlClass}
|
|
required
|
|
minLength={10}
|
|
maxLength={4000}
|
|
value={comparison.data.reason}
|
|
onChange={(e) =>
|
|
comparison.setData(
|
|
'reason',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<Button disabled={comparison.processing}>
|
|
Guardar para novos processos
|
|
</Button>
|
|
</form>
|
|
</Panel>
|
|
<Panel title="IVA para novas linhas">
|
|
<p className="mb-4 text-sm">
|
|
Taxa inicial para novas linhas de requisição.
|
|
Pode ser ajustada em cada artigo conforme o
|
|
enquadramento fiscal. As linhas e propostas
|
|
anteriores conservam a taxa registada. Os
|
|
descontos unitários incidem antes do IVA.
|
|
</p>
|
|
<form
|
|
className="space-y-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
tax.post(base + '/tax', {
|
|
preserveState: 'errors',
|
|
preserveScroll: true,
|
|
});
|
|
}}
|
|
>
|
|
<FormErrors errors={tax.errors} />
|
|
<label className="block space-y-1">
|
|
Taxa de IVA inicial (%)
|
|
<input
|
|
className={controlClass}
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
step="0.01"
|
|
required
|
|
value={tax.data.default_tax_rate}
|
|
onChange={(e) =>
|
|
tax.setData(
|
|
'default_tax_rate',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="block space-y-1">
|
|
Fundamento e âmbito fiscal
|
|
<textarea
|
|
className={controlClass}
|
|
required
|
|
minLength={10}
|
|
maxLength={4000}
|
|
value={tax.data.reason}
|
|
onChange={(e) =>
|
|
tax.setData(
|
|
'reason',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<Button disabled={tax.processing}>
|
|
Guardar taxa inicial
|
|
</Button>
|
|
</form>
|
|
</Panel>
|
|
<Panel title="Regras de aprovação da empresa">
|
|
<p className="mb-4 text-sm">
|
|
{companyRules.provisional
|
|
? 'Os valores actuais são provisórios; confirme e guarde as regras da empresa.'
|
|
: 'Regras definidas pela empresa.'}{' '}
|
|
Alterações aplicam-se a novas submissões. Os
|
|
processos já submetidos conservam as regras
|
|
anteriores.
|
|
</p>
|
|
<form
|
|
className="grid gap-4 sm:grid-cols-2"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
rules.post(base + '/rules', {
|
|
preserveState: 'errors',
|
|
preserveScroll: true,
|
|
});
|
|
}}
|
|
>
|
|
<div className="sm:col-span-2">
|
|
<FormErrors errors={rules.errors} />
|
|
</div>
|
|
<label className="space-y-1">
|
|
Primeiro limite (MZN)
|
|
<input
|
|
required
|
|
inputMode="decimal"
|
|
pattern="[0-9]+(\.[0-9]{1,2})?"
|
|
className={controlClass}
|
|
value={rules.data.first_limit}
|
|
onChange={(e) =>
|
|
rules.setData(
|
|
'first_limit',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="space-y-1">
|
|
Segundo limite (MZN)
|
|
<input
|
|
required
|
|
inputMode="decimal"
|
|
pattern="[0-9]+(\.[0-9]{1,2})?"
|
|
className={controlClass}
|
|
value={rules.data.second_limit}
|
|
onChange={(e) =>
|
|
rules.setData(
|
|
'second_limit',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="flex items-center gap-2 sm:col-span-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={rules.data.includes_tax}
|
|
onChange={(e) =>
|
|
rules.setData(
|
|
'includes_tax',
|
|
e.target.checked,
|
|
)
|
|
}
|
|
/>
|
|
Aplicar os limites ao valor com imposto
|
|
</label>
|
|
<p className="text-sm sm:col-span-2">
|
|
Supervisão do departamento na primeira
|
|
etapa; acima do primeiro limite
|
|
acrescenta-se Compras; acima do segundo
|
|
acrescenta-se Finanças. Equipamentos mantêm
|
|
a conferência financeira e técnica. Atribua
|
|
estas funções às pessoas através dos perfis.
|
|
</p>
|
|
<label className="space-y-1 sm:col-span-2">
|
|
Fundamentação
|
|
<textarea
|
|
required
|
|
minLength={10}
|
|
className={controlClass}
|
|
value={rules.data.reason}
|
|
onChange={(e) =>
|
|
rules.setData(
|
|
'reason',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<Button disabled={rules.processing}>
|
|
Guardar regras da empresa
|
|
</Button>
|
|
</form>
|
|
</Panel>
|
|
<Panel title="Perfis e permissões da empresa">
|
|
<div className="mb-4 flex flex-wrap gap-3">
|
|
{profiles.map((p) => (
|
|
<Button
|
|
key={p.id}
|
|
variant="outline"
|
|
onClick={() =>
|
|
profile.setData({
|
|
version,
|
|
id: String(p.id),
|
|
name: p.name,
|
|
permissions: p.permissions,
|
|
})
|
|
}
|
|
>
|
|
{p.name}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<form
|
|
className="space-y-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
profile.transform((values) => ({
|
|
...values,
|
|
id: values.id || null,
|
|
}));
|
|
profile.post(base + '/profile', {
|
|
preserveState: 'errors',
|
|
preserveScroll: true,
|
|
});
|
|
}}
|
|
>
|
|
<FormErrors errors={profile.errors} />
|
|
<label className="block space-y-1">
|
|
Nome do perfil
|
|
<input
|
|
required
|
|
maxLength={120}
|
|
className={controlClass}
|
|
value={profile.data.name}
|
|
onChange={(e) =>
|
|
profile.setData(
|
|
'name',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<fieldset className="grid gap-3 sm:grid-cols-2">
|
|
<legend className="mb-2 font-medium">
|
|
Permissões funcionais
|
|
</legend>
|
|
{Object.entries(permissions).map(
|
|
([key, label]) => (
|
|
<label
|
|
key={key}
|
|
className="flex items-center gap-2 text-sm"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={profile.data.permissions.includes(
|
|
key,
|
|
)}
|
|
onChange={(e) =>
|
|
profile.setData(
|
|
'permissions',
|
|
e.target.checked
|
|
? [
|
|
...profile
|
|
.data
|
|
.permissions,
|
|
key,
|
|
]
|
|
: profile.data.permissions.filter(
|
|
(p) =>
|
|
p !==
|
|
key,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
{label}
|
|
</label>
|
|
),
|
|
)}
|
|
</fieldset>
|
|
<div className="flex gap-3">
|
|
<Button disabled={profile.processing}>
|
|
{profile.data.id
|
|
? 'Actualizar perfil'
|
|
: 'Criar perfil'}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() =>
|
|
profile.setData({
|
|
version,
|
|
id: '',
|
|
name: '',
|
|
permissions: [],
|
|
})
|
|
}
|
|
>
|
|
Novo perfil
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Panel>
|
|
<Panel title="Utilizadores e atribuições">
|
|
<div className="space-y-3">
|
|
{users.map((person) => (
|
|
<Assignment
|
|
key={`${person.id}-${version}`}
|
|
person={person}
|
|
profiles={profiles}
|
|
members={memberships}
|
|
version={version}
|
|
/>
|
|
))}
|
|
</div>
|
|
<details className="mt-5 rounded border p-4">
|
|
<summary className="cursor-pointer font-medium">
|
|
Criar utilizador
|
|
</summary>
|
|
<form
|
|
className="mt-4 grid gap-4 sm:grid-cols-2"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
user.post(base + '/user', {
|
|
preserveState: 'errors',
|
|
preserveScroll: true,
|
|
onSuccess: () =>
|
|
user.reset(
|
|
'password',
|
|
'password_confirmation',
|
|
),
|
|
});
|
|
}}
|
|
>
|
|
<div className="sm:col-span-2">
|
|
<FormErrors errors={user.errors} />
|
|
</div>
|
|
{(
|
|
[
|
|
'name',
|
|
'email',
|
|
'password',
|
|
'password_confirmation',
|
|
] as const
|
|
).map((key) => (
|
|
<label className="space-y-1" key={key}>
|
|
{
|
|
{
|
|
name: 'Nome',
|
|
email: 'Email',
|
|
password:
|
|
'Palavra-passe inicial',
|
|
password_confirmation:
|
|
'Confirmar palavra-passe',
|
|
}[key]
|
|
}
|
|
<input
|
|
required
|
|
type={
|
|
key.includes('password')
|
|
? 'password'
|
|
: key === 'email'
|
|
? 'email'
|
|
: 'text'
|
|
}
|
|
minLength={
|
|
key.includes('password')
|
|
? 12
|
|
: undefined
|
|
}
|
|
autoComplete={
|
|
key.includes('password')
|
|
? 'new-password'
|
|
: undefined
|
|
}
|
|
className={controlClass}
|
|
value={user.data[key]}
|
|
onChange={(e) =>
|
|
user.setData(
|
|
key,
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
<label className="space-y-1">
|
|
Departamento
|
|
<select
|
|
required
|
|
className={controlClass}
|
|
value={user.data.department_id}
|
|
onChange={(e) =>
|
|
user.setData(
|
|
'department_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Escolher</option>
|
|
{departments.map((d) => (
|
|
<option key={d.id} value={d.id}>
|
|
{d.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<div className="space-y-2 sm:col-span-2">
|
|
<p>Perfis atribuídos</p>
|
|
<Choices
|
|
profiles={profiles}
|
|
selected={user.data.profile_ids}
|
|
change={(ids) =>
|
|
user.setData('profile_ids', ids)
|
|
}
|
|
/>
|
|
</div>
|
|
<label className="space-y-1 sm:col-span-2">
|
|
Fundamento
|
|
<textarea
|
|
required
|
|
minLength={10}
|
|
className={controlClass}
|
|
value={user.data.reason}
|
|
onChange={(e) =>
|
|
user.setData(
|
|
'reason',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<Button disabled={user.processing}>
|
|
Criar utilizador
|
|
</Button>
|
|
</form>
|
|
</details>
|
|
</Panel>
|
|
<Panel title="Organização e centros de custo">
|
|
<div className="grid gap-5 sm:grid-cols-2">
|
|
<form
|
|
className="space-y-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
department.post(
|
|
'/empresa/departamentos',
|
|
{ preserveScroll: true },
|
|
);
|
|
}}
|
|
>
|
|
<FormErrors errors={department.errors} />
|
|
<label className="block">
|
|
Novo departamento
|
|
<input
|
|
required
|
|
className={controlClass}
|
|
value={department.data.name}
|
|
onChange={(e) =>
|
|
department.setData(
|
|
'name',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<Button disabled={department.processing}>
|
|
Adicionar departamento
|
|
</Button>
|
|
<p className="text-sm">
|
|
{departments
|
|
.map((d) => d.name)
|
|
.join(' · ')}
|
|
</p>
|
|
</form>
|
|
<form
|
|
className="space-y-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
center.post(
|
|
'/empresa/centros-de-custo',
|
|
{ preserveScroll: true },
|
|
);
|
|
}}
|
|
>
|
|
<FormErrors errors={center.errors} />
|
|
<label className="block">
|
|
Departamento
|
|
<select
|
|
required
|
|
className={controlClass}
|
|
value={center.data.department_id}
|
|
onChange={(e) =>
|
|
center.setData(
|
|
'department_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Escolher</option>
|
|
{departments.map((d) => (
|
|
<option key={d.id} value={d.id}>
|
|
{d.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="block">
|
|
Código
|
|
<input
|
|
required
|
|
className={controlClass}
|
|
value={center.data.code}
|
|
onChange={(e) =>
|
|
center.setData(
|
|
'code',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
Centro de custo
|
|
<input
|
|
required
|
|
className={controlClass}
|
|
value={center.data.name}
|
|
onChange={(e) =>
|
|
center.setData(
|
|
'name',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<Button disabled={center.processing}>
|
|
Adicionar centro
|
|
</Button>
|
|
<p className="text-sm">
|
|
{costCenters
|
|
.map((c) => c.code + ' · ' + c.name)
|
|
.join(' / ')}
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</Panel>
|
|
<Panel title="Manutenção TEMPO">
|
|
<p className="text-sm">
|
|
Atribua a uma conta nominativa um perfil sem
|
|
administração, assinale manutenção e defina a
|
|
expiração. O acesso fica limitado a diagnóstico
|
|
e consulta, é revogado removendo as atribuições
|
|
e termina automaticamente na data indicada. As
|
|
atribuições, alterações e operações mantêm
|
|
autoria e auditoria; não existe palavra-passe
|
|
universal.
|
|
</p>
|
|
</Panel>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|