Implementa reforcos orcamentais com aprovacao independente
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { FormErrors, controlClass } from '@/components/procurement';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
export function ReinforcementProposal({ budgetId }: { budgetId: number }) {
|
||||
const form = useForm({ reference: '', amount: '', reason: '' });
|
||||
return (
|
||||
<details className="mt-4 rounded-lg border p-4">
|
||||
<summary className="cursor-pointer font-semibold">
|
||||
Propor reforço desta dotação
|
||||
</summary>
|
||||
<form
|
||||
className="mt-4 grid gap-3"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(`/orcamentos/${budgetId}/reforcos`, {
|
||||
onSuccess: () => form.reset(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<p className="text-sm">
|
||||
Aumenta o saldo apenas depois da aprovação por outra pessoa.
|
||||
Mantém a moeda e a base de IVA da dotação.
|
||||
</p>
|
||||
<label>
|
||||
Referência interna única na empresa
|
||||
<Input
|
||||
required
|
||||
maxLength={100}
|
||||
value={form.data.reference}
|
||||
onChange={(e) =>
|
||||
form.setData('reference', e.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Montante do reforço (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}>
|
||||
Enviar para aprovação independente
|
||||
</Button>
|
||||
</form>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
export function ReinforcementReview({ id }: { id: number }) {
|
||||
const form = useForm({ decision: 'approved', reason: '' });
|
||||
return (
|
||||
<form
|
||||
className="mt-4 grid gap-3"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(`/reforcos/${id}/decidir`);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<label>
|
||||
Decisão
|
||||
<select
|
||||
className={controlClass}
|
||||
value={form.data.decision}
|
||||
onChange={(e) => form.setData('decision', e.target.value)}
|
||||
>
|
||||
<option value="approved">Aprovar reforço</option>
|
||||
<option value="rejected">Rejeitar reforço</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Fundamento da decisão
|
||||
<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 decisão imutável</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -12,5 +12,6 @@ export const previewPaths: Record<string, string> = {
|
||||
invoices: '/consulta/facturas',
|
||||
bank: '/consulta/dados-bancarios',
|
||||
payments: '/consulta/pagamentos',
|
||||
reinforcements: '/consulta/reforcos',
|
||||
appearance: '/consulta/aparencia',
|
||||
};
|
||||
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { money } from '@/types/procurement';
|
||||
import {
|
||||
ReinforcementProposal,
|
||||
ReinforcementReview,
|
||||
} from '@/components/budget-reinforcements';
|
||||
import { InventoryPagination } from '@/components/inventory';
|
||||
import type { Page } from '@/types/inventory';
|
||||
|
||||
type Budget = {
|
||||
id: number;
|
||||
@@ -22,13 +28,27 @@ type Budget = {
|
||||
approved_by: number | null;
|
||||
reason: string;
|
||||
includes_tax: boolean;
|
||||
reinforced_minor: number;
|
||||
approved_total_minor: number;
|
||||
};
|
||||
export default function Budgets({
|
||||
centers,
|
||||
budgets,
|
||||
reinforcements,
|
||||
}: {
|
||||
centers: { id: number; code: string; name: string }[];
|
||||
budgets: Budget[];
|
||||
reinforcements: Page<{
|
||||
id: number;
|
||||
budget_id: number;
|
||||
reference: string;
|
||||
amount_minor: number;
|
||||
reason: string;
|
||||
status: string;
|
||||
created_by: number;
|
||||
reviewed_by: number | null;
|
||||
review_reason: string | null;
|
||||
}>;
|
||||
}) {
|
||||
const { auth } = usePage().props;
|
||||
const form = useForm({
|
||||
@@ -57,7 +77,7 @@ export default function Budgets({
|
||||
</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."
|
||||
description="Outra pessoa de Administração ou Finanças deve aprovar. A dotação inicial é preservada; reforços têm aprovação própria."
|
||||
>
|
||||
<form
|
||||
className="grid gap-4"
|
||||
@@ -165,6 +185,10 @@ export default function Budgets({
|
||||
{money(b.consumed_minor)} · Disponível:{' '}
|
||||
{money(b.available_minor)}
|
||||
</p>
|
||||
<p>
|
||||
Reforços aprovados: {money(b.reinforced_minor)} ·
|
||||
Dotação acumulada: {money(b.approved_total_minor)}
|
||||
</p>
|
||||
<p className="my-3">{b.reason}</p>
|
||||
{!b.approved_by && b.created_by !== auth.user.id && (
|
||||
<Button
|
||||
@@ -175,8 +199,56 @@ export default function Budgets({
|
||||
Aprovar dotação
|
||||
</Button>
|
||||
)}
|
||||
{b.approved_by && (
|
||||
<ReinforcementProposal budgetId={b.id} />
|
||||
)}
|
||||
</Panel>
|
||||
))}
|
||||
<Panel
|
||||
title="Histórico de reforços"
|
||||
description="Pendentes e rejeitados não alteram o saldo. O valor inicial nunca é reescrito."
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{reinforcements.data.map((r) => (
|
||||
<article
|
||||
key={r.id}
|
||||
className="rounded border p-4 text-sm"
|
||||
>
|
||||
<p className="font-semibold">
|
||||
{r.reference} · Dotação #{r.budget_id} ·{' '}
|
||||
{money(r.amount_minor)}
|
||||
</p>
|
||||
<p>
|
||||
{{
|
||||
pending: 'Pendente',
|
||||
approved: 'Aprovado',
|
||||
rejected: 'Rejeitado',
|
||||
}[r.status] ?? r.status}{' '}
|
||||
· Proposto por #{r.created_by}
|
||||
{r.reviewed_by
|
||||
? ` · Decidido por #${r.reviewed_by}`
|
||||
: ''}
|
||||
</p>
|
||||
<p className="my-2 whitespace-pre-wrap">
|
||||
{r.reason}
|
||||
</p>
|
||||
{r.review_reason && (
|
||||
<p className="whitespace-pre-wrap">
|
||||
Decisão: {r.review_reason}
|
||||
</p>
|
||||
)}
|
||||
{r.status === 'pending' &&
|
||||
r.created_by !== auth.user.id && (
|
||||
<ReinforcementReview id={r.id} />
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
{reinforcements.data.length === 0 && (
|
||||
<p>Sem reforços nesta empresa.</p>
|
||||
)}
|
||||
</div>
|
||||
<InventoryPagination page={reinforcements} />
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -121,6 +121,7 @@ const navigation = [
|
||||
{ id: 'invoices', title: 'Facturas e conferência', icon: FileText },
|
||||
{ id: 'bank', title: 'Dados bancários', icon: ShieldCheck },
|
||||
{ id: 'payments', title: 'Pagamentos externos', icon: FileText },
|
||||
{ id: 'reinforcements', title: 'Reforços orçamentais', icon: FileText },
|
||||
{ id: 'appearance', title: 'Cores e componentes', icon: Palette },
|
||||
];
|
||||
type Detail = { title: string; subtitle: string; rows: [string, string][] };
|
||||
@@ -129,7 +130,10 @@ const navigationGroups = [
|
||||
{ title: 'Compras', ids: ['requests', 'suppliers', 'quotes', 'orders'] },
|
||||
{ title: 'Armazém', ids: ['receipts', 'stock', 'operations', 'equipment'] },
|
||||
{ title: 'Controlo', ids: ['conflicts'] },
|
||||
{ title: 'Finanças', ids: ['invoices', 'bank', 'payments'] },
|
||||
{
|
||||
title: 'Finanças',
|
||||
ids: ['invoices', 'bank', 'payments', 'reinforcements'],
|
||||
},
|
||||
{ title: 'Apresentação', ids: ['appearance'] },
|
||||
];
|
||||
export default function Preview({
|
||||
@@ -665,6 +669,34 @@ export default function Preview({
|
||||
)}
|
||||
{screen === 'operations' && <OperationsDemo />}
|
||||
{screen === 'equipment' && <EquipmentDemo />}
|
||||
{screen === 'reinforcements' && (
|
||||
<Panel
|
||||
title="Reforço orçamental · exemplo fictício"
|
||||
description="Valores ilustrativos; cada empresa define as suas dotações. Sem consultas ou gravações de negócio."
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>
|
||||
Dotação inicial fictícia de 1.000,00
|
||||
MZN, com 300,00 MZN utilizados:
|
||||
disponível de 700,00 MZN.
|
||||
</p>
|
||||
<p>
|
||||
Um reforço proposto de 400,00 MZN fica
|
||||
pendente. Só depois da aprovação por
|
||||
outra pessoa a dotação acumulada passa a
|
||||
1.400,00 MZN e o disponível a 1.100,00
|
||||
MZN.
|
||||
</p>
|
||||
<p>
|
||||
O valor inicial, a base de IVA, o centro
|
||||
de custo e o exercício são preservados.
|
||||
Repetir uma aprovação não duplica a
|
||||
verba. Rejeições ficam no histórico sem
|
||||
alterar o saldo.
|
||||
</p>
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
{screen === 'payments' && (
|
||||
<Panel
|
||||
title="Pagamento externo · exemplo fictício"
|
||||
|
||||
Reference in New Issue
Block a user