This commit is contained in:
Luciano Milani
2026-07-02 12:47:55 +01:00
commit 5de37bb512
132 changed files with 28495 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { apiFetch } from '@/lib/api'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import type { Invoice, WorkOrder } from '@/lib/types'
const TYPE_LABELS: Record<string, string> = { quote: 'Orçamento', invoice: 'Fatura' }
export default function InvoicesPage() {
const qc = useQueryClient()
const [showGenerate, setShowGenerate] = useState(false)
const [selectedWO, setSelectedWO] = useState('')
const [docType, setDocType] = useState<'quote' | 'invoice'>('quote')
const { data: invoices = [], isLoading } = useQuery<Invoice[]>({
queryKey: ['invoices'],
queryFn: () => apiFetch<Invoice[]>('/invoices'),
})
const { data: workOrders = [] } = useQuery<WorkOrder[]>({
queryKey: ['work-orders-for-invoice'],
queryFn: () => apiFetch<WorkOrder[]>('/work-orders'),
enabled: showGenerate,
})
const eligibleWOs = workOrders.filter((wo) =>
wo.status === 'open' || wo.status === 'in_progress' || wo.status === 'completed'
)
const generate = useMutation({
mutationFn: () =>
apiFetch<Invoice>('/invoices', {
method: 'POST',
body: JSON.stringify({ work_order_id: selectedWO, type: docType }),
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['invoices'] })
qc.invalidateQueries({ queryKey: ['work-orders'] })
setShowGenerate(false)
setSelectedWO('')
},
})
return (
<div>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-white">Faturação</h1>
<p className="text-slate-400 text-sm mt-0.5">{invoices.length} documentos</p>
</div>
<Button onClick={() => setShowGenerate(true)}>Gerar Documento</Button>
</div>
{showGenerate && (
<div className="mb-6 bg-slate-800 rounded-lg border border-slate-700 p-6">
<h2 className="text-lg font-semibold text-white mb-4">Gerar Documento</h2>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-sm text-slate-300">Tipo</label>
<select
value={docType}
onChange={(e) => setDocType(e.target.value as 'quote' | 'invoice')}
className="w-full rounded-md border border-slate-600 bg-slate-900 text-white px-3 py-2 text-sm"
>
<option value="quote">Orçamento</option>
<option value="invoice">Fatura</option>
</select>
</div>
<div className="space-y-1">
<label className="text-sm text-slate-300">Ordem de Trabalho</label>
<select
value={selectedWO}
onChange={(e) => setSelectedWO(e.target.value)}
className="w-full rounded-md border border-slate-600 bg-slate-900 text-white px-3 py-2 text-sm"
>
<option value=""> Seleccionar OT </option>
{eligibleWOs.map((wo) => (
<option key={wo.id} value={wo.id}>
#{wo.number} ({wo.status})
</option>
))}
</select>
</div>
</div>
{generate.error && (
<p className="text-red-400 text-sm mt-3">{(generate.error as Error).message}</p>
)}
<div className="flex gap-2 justify-end mt-4">
<Button variant="outline" onClick={() => { setShowGenerate(false); setSelectedWO('') }}>
Cancelar
</Button>
<Button
onClick={() => generate.mutate()}
disabled={!selectedWO || generate.isPending}
>
{generate.isPending ? 'A gerar...' : 'Gerar PDF'}
</Button>
</div>
</div>
)}
{isLoading ? (
<p className="text-slate-400">A carregar...</p>
) : invoices.length === 0 ? (
<p className="text-slate-500 text-sm">Nenhum documento gerado.</p>
) : (
<div className="rounded-lg border border-slate-700 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-slate-800">
<tr>
<th className="text-left px-4 py-3 text-slate-400 font-medium"></th>
<th className="text-left px-4 py-3 text-slate-400 font-medium">Tipo</th>
<th className="text-left px-4 py-3 text-slate-400 font-medium">OT</th>
<th className="text-left px-4 py-3 text-slate-400 font-medium">Emitida</th>
<th className="px-4 py-3"></th>
</tr>
</thead>
<tbody>
{invoices.map((inv) => (
<tr key={inv.id} className="border-t border-slate-700 hover:bg-slate-800/50">
<td className="px-4 py-3 text-white font-mono">#{inv.number}</td>
<td className="px-4 py-3">
<Badge variant={inv.type === 'invoice' ? 'default' : 'secondary'}>
{TYPE_LABELS[inv.type]}
</Badge>
</td>
<td className="px-4 py-3 text-slate-400 font-mono text-xs">
{inv.work_order_id.slice(0, 8)}
</td>
<td className="px-4 py-3 text-slate-400">
{new Intl.DateTimeFormat('pt-PT').format(new Date(inv.issued_at))}
</td>
<td className="px-4 py-3 text-right">
<Button
size="sm"
variant="outline"
onClick={() => window.open(`/api/v1/invoices/${inv.id}/pdf`, '_blank')}
>
Descarregar PDF
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}