180 lines
7.3 KiB
TypeScript
180 lines
7.3 KiB
TypeScript
import { Head, router, useForm, usePage } from '@inertiajs/react';
|
|
import {
|
|
Panel,
|
|
PageTitle,
|
|
FormErrors,
|
|
controlClass,
|
|
} from '@/components/procurement';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { money } from '@/types/procurement';
|
|
|
|
type Budget = {
|
|
id: number;
|
|
cost_center_id: number;
|
|
year: number;
|
|
amount_minor: number;
|
|
reserved_minor: number;
|
|
committed_minor: number;
|
|
available_minor: number;
|
|
created_by: number;
|
|
approved_by: number | null;
|
|
reason: string;
|
|
includes_tax: boolean;
|
|
};
|
|
export default function Budgets({
|
|
centers,
|
|
budgets,
|
|
}: {
|
|
centers: { id: number; code: string; name: string }[];
|
|
budgets: Budget[];
|
|
}) {
|
|
const { auth } = usePage().props;
|
|
const form = useForm({
|
|
cost_center_id: '',
|
|
year: new Date().getFullYear(),
|
|
amount: '',
|
|
reason: '',
|
|
includes_tax: '',
|
|
});
|
|
return (
|
|
<>
|
|
<Head title="Orçamentos" />
|
|
<div className="mx-auto grid w-full max-w-6xl gap-6 p-6">
|
|
<PageTitle
|
|
eyebrow="SGC · Finanças"
|
|
title="Orçamentos"
|
|
description="Dotação anual por empresa e centro de custo, em MZN. A empresa define a base de IVA."
|
|
/>
|
|
<p className="rounded-lg border border-amber-300 p-4 text-sm">
|
|
Desenvolvimento. O orçamento continua sem validação
|
|
operacional. Centros sem dotação continuam no circuito
|
|
anterior, sem controlo orçamental. Facturas e pagamentos
|
|
ainda não implementados.
|
|
</p>
|
|
<Panel
|
|
title="Preparar dotação"
|
|
description="Outra pessoa de Administração ou Finanças deve aprovar. Não existem alterações ou reforços nesta etapa."
|
|
>
|
|
<form
|
|
className="grid gap-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.transform((data) => ({
|
|
...data,
|
|
includes_tax: data.includes_tax === '1',
|
|
}));
|
|
form.post('/orcamentos');
|
|
}}
|
|
>
|
|
<FormErrors errors={form.errors} />
|
|
<label>
|
|
Base de controlo
|
|
<select
|
|
required
|
|
className={controlClass}
|
|
value={form.data.includes_tax}
|
|
onChange={(e) =>
|
|
form.setData('includes_tax', e.target.value)
|
|
}
|
|
>
|
|
<option value="">Decisão da empresa</option>
|
|
<option value="1">Com IVA</option>
|
|
<option value="0">Sem IVA</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Centro de custo
|
|
<select
|
|
required
|
|
className={controlClass}
|
|
value={form.data.cost_center_id}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'cost_center_id',
|
|
e.target.value,
|
|
)
|
|
}
|
|
>
|
|
<option value="">Seleccionar</option>
|
|
{centers.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.code} · {c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Exercício
|
|
<Input
|
|
required
|
|
type="number"
|
|
min="2026"
|
|
max="2100"
|
|
value={form.data.year}
|
|
onChange={(e) =>
|
|
form.setData('year', Number(e.target.value))
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Dotação na base seleccionada (MZN)
|
|
<Input
|
|
required
|
|
inputMode="decimal"
|
|
value={form.data.amount}
|
|
onChange={(e) =>
|
|
form.setData('amount', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Fundamento
|
|
<textarea
|
|
required
|
|
className="w-full rounded border p-3"
|
|
value={form.data.reason}
|
|
onChange={(e) =>
|
|
form.setData('reason', e.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
<Button disabled={form.processing}>
|
|
Guardar para aprovação independente
|
|
</Button>
|
|
</form>
|
|
</Panel>
|
|
{budgets.map((b) => (
|
|
<Panel
|
|
key={b.id}
|
|
title={`${centers.find((c) => c.id === b.cost_center_id)?.code ?? b.cost_center_id} · ${b.year}`}
|
|
>
|
|
<p>
|
|
{b.approved_by
|
|
? 'Aprovado'
|
|
: 'Pendente de aprovação'}{' '}
|
|
· Dotação: {money(b.amount_minor)} · Base:{' '}
|
|
{b.includes_tax ? 'com IVA' : 'sem IVA'}
|
|
</p>
|
|
<p>
|
|
Reservado: {money(b.reserved_minor)} · Comprometido:{' '}
|
|
{money(b.committed_minor)} · Disponível:{' '}
|
|
{money(b.available_minor)}
|
|
</p>
|
|
<p className="my-3">{b.reason}</p>
|
|
{!b.approved_by && b.created_by !== auth.user.id && (
|
|
<Button
|
|
onClick={() =>
|
|
router.post(`/orcamentos/${b.id}/aprovar`)
|
|
}
|
|
>
|
|
Aprovar dotação
|
|
</Button>
|
|
)}
|
|
</Panel>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|