@hed-hog/operations 0.0.331 → 0.0.332

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +54 -0
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-collaborators.controller.js +100 -0
  4. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  5. package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
  6. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
  7. package/dist/dto/create-collaborator-invoice.dto.js +55 -0
  8. package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
  9. package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
  10. package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
  11. package/dist/dto/create-collaborator-payment.dto.js +50 -0
  12. package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
  13. package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
  14. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
  15. package/dist/dto/list-collaborator-invoice.dto.js +8 -0
  16. package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
  17. package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
  18. package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
  19. package/dist/dto/list-collaborator-payment.dto.js +8 -0
  20. package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
  21. package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
  22. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
  23. package/dist/dto/update-collaborator-invoice.dto.js +9 -0
  24. package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
  25. package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
  26. package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
  27. package/dist/dto/update-collaborator-payment.dto.js +9 -0
  28. package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
  29. package/dist/operations.service.d.ts +76 -0
  30. package/dist/operations.service.d.ts.map +1 -1
  31. package/dist/operations.service.js +235 -5
  32. package/dist/operations.service.js.map +1 -1
  33. package/hedhog/data/menu.yaml +27 -8
  34. package/hedhog/data/route.yaml +72 -0
  35. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -3
  36. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
  37. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
  38. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +86 -87
  39. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +218 -10
  40. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +710 -26
  41. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +158 -38
  42. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +807 -803
  43. package/hedhog/frontend/app/_lib/api.ts.ejs +631 -480
  44. package/hedhog/frontend/app/_lib/types.ts.ejs +6 -5
  45. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
  46. package/hedhog/frontend/app/my-projects/page.tsx.ejs +16 -2
  47. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +95 -157
  48. package/hedhog/frontend/app/projects/page.tsx.ejs +42 -6
  49. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
  50. package/hedhog/frontend/messages/en.json +96 -2
  51. package/hedhog/frontend/messages/pt.json +96 -2
  52. package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
  53. package/hedhog/table/operations_collaborator_payment.yaml +32 -0
  54. package/package.json +5 -5
  55. package/src/controllers/operations-collaborators.controller.ts +117 -8
  56. package/src/dto/create-collaborator-invoice.dto.ts +39 -0
  57. package/src/dto/create-collaborator-payment.dto.ts +35 -0
  58. package/src/dto/list-collaborator-invoice.dto.ts +3 -0
  59. package/src/dto/list-collaborator-payment.dto.ts +3 -0
  60. package/src/dto/update-collaborator-invoice.dto.ts +6 -0
  61. package/src/dto/update-collaborator-payment.dto.ts +6 -0
  62. package/src/operations.service.ts +328 -5
@@ -134,6 +134,25 @@
134
134
  - where:
135
135
  slug: admin-operations-director
136
136
 
137
+ - menu_id:
138
+ where:
139
+ slug: /operations
140
+ icon: calendar-range
141
+ url: /operations/tasks-gantt
142
+ name:
143
+ en: Tasks Gantt
144
+ pt: Gantt de Tarefas
145
+ slug: /operations/tasks-gantt
146
+ order: 118
147
+ relations:
148
+ role:
149
+ - where:
150
+ slug: admin
151
+ - where:
152
+ slug: admin-operations-supervisor
153
+ - where:
154
+ slug: admin-operations-director
155
+
137
156
  - menu_id:
138
157
  where:
139
158
  slug: /operations
@@ -143,7 +162,7 @@
143
162
  en: Approvals
144
163
  pt: Aprovações
145
164
  slug: /operations/approvals
146
- order: 118
165
+ order: 119
147
166
  relations:
148
167
  role:
149
168
  - where:
@@ -162,7 +181,7 @@
162
181
  en: Time Off
163
182
  pt: Folgas
164
183
  slug: /operations/time-off
165
- order: 119
184
+ order: 120
166
185
  relations:
167
186
  role:
168
187
  - where:
@@ -183,7 +202,7 @@
183
202
  en: Schedule Adjustments
184
203
  pt: Ajustes de Jornada
185
204
  slug: /operations/schedule-adjustments
186
- order: 120
205
+ order: 121
187
206
  relations:
188
207
  role:
189
208
  - where:
@@ -204,7 +223,7 @@
204
223
  en: Departments
205
224
  pt: Departamentos
206
225
  slug: /operations/departments
207
- order: 121
226
+ order: 122
208
227
  relations:
209
228
  role:
210
229
  - where:
@@ -221,7 +240,7 @@
221
240
  en: Collaborator Types
222
241
  pt: Tipos de Vinculo
223
242
  slug: /operations/collaborator-types
224
- order: 122
243
+ order: 123
225
244
  relations:
226
245
  role:
227
246
  - where:
@@ -237,7 +256,7 @@
237
256
  en: Reports
238
257
  pt: Relatórios
239
258
  slug: /operations/reports
240
- order: 123
259
+ order: 124
241
260
  relations:
242
261
  role:
243
262
  - where:
@@ -256,7 +275,7 @@
256
275
  en: Projects
257
276
  pt: Projetos
258
277
  slug: /operations/reports/projects
259
- order: 124
278
+ order: 125
260
279
  relations:
261
280
  role:
262
281
  - where:
@@ -275,7 +294,7 @@
275
294
  en: Collaborators
276
295
  pt: Colaboradores
277
296
  slug: /operations/reports/collaborators
278
- order: 125
297
+ order: 126
279
298
  relations:
280
299
  role:
281
300
  - where:
@@ -101,6 +101,78 @@
101
101
  - where:
102
102
  slug: admin-operations-director
103
103
 
104
+ - url: /operations/collaborators/:id/payment-history
105
+ method: GET
106
+ relations:
107
+ role:
108
+ - where:
109
+ slug: admin
110
+ - where:
111
+ slug: admin-operations-director
112
+
113
+ - url: /operations/collaborators/:id/payment-history
114
+ method: POST
115
+ relations:
116
+ role:
117
+ - where:
118
+ slug: admin
119
+ - where:
120
+ slug: admin-operations-director
121
+
122
+ - url: /operations/collaborators/:id/payment-history/:paymentId
123
+ method: PATCH
124
+ relations:
125
+ role:
126
+ - where:
127
+ slug: admin
128
+ - where:
129
+ slug: admin-operations-director
130
+
131
+ - url: /operations/collaborators/:id/payment-history/:paymentId
132
+ method: DELETE
133
+ relations:
134
+ role:
135
+ - where:
136
+ slug: admin
137
+ - where:
138
+ slug: admin-operations-director
139
+
140
+ - url: /operations/collaborators/:id/invoices
141
+ method: GET
142
+ relations:
143
+ role:
144
+ - where:
145
+ slug: admin
146
+ - where:
147
+ slug: admin-operations-director
148
+
149
+ - url: /operations/collaborators/:id/invoices
150
+ method: POST
151
+ relations:
152
+ role:
153
+ - where:
154
+ slug: admin
155
+ - where:
156
+ slug: admin-operations-director
157
+
158
+ - url: /operations/collaborators/:id/invoices/:invoiceId
159
+ method: PATCH
160
+ relations:
161
+ role:
162
+ - where:
163
+ slug: admin
164
+ - where:
165
+ slug: admin-operations-director
166
+
167
+ - url: /operations/collaborators/:id/invoices/:invoiceId
168
+ method: DELETE
169
+ relations:
170
+ role:
171
+ - where:
172
+ slug: admin
173
+ - where:
174
+ slug: admin-operations-director
175
+
104
176
  - url: /operations/collaborators/:id
105
177
  method: GET
106
178
  relations:
@@ -111,6 +111,8 @@ import {
111
111
  trimToNull,
112
112
  } from '../_lib/utils/forms';
113
113
  import { CollaboratorCostsSection } from './collaborator-costs-section';
114
+ import { CollaboratorInvoicesTab } from './collaborator-invoices-tab';
115
+ import { CollaboratorPaymentHistoryTab } from './collaborator-payment-history-tab';
114
116
  import { CollaboratorTasksTab } from './collaborator-tasks-tab';
115
117
  import { CollaboratorTimesheetsTab } from './collaborator-timesheets-tab';
116
118
  import { DepartmentPicker } from './department-picker';
@@ -2143,9 +2145,7 @@ export function CollaboratorFormScreen({
2143
2145
 
2144
2146
  const formContent = isSheetMode ? (
2145
2147
  <Tabs defaultValue="details" className="w-full">
2146
- <TabsList
2147
- className={`mx-4 mb-2 grid w-[calc(100%-2rem)] ${!isCreateMode && !isHourlyType ? 'grid-cols-7' : isCreateMode && isHourlyType ? 'grid-cols-5' : 'grid-cols-6'}`}
2148
- >
2148
+ <TabsList className="mx-4 mb-2 flex w-[calc(100%-2rem)] flex-wrap gap-0.5 h-auto">
2149
2149
  <TabsTrigger value="details">{t('tabs.details')}</TabsTrigger>
2150
2150
  <TabsTrigger value="costs">{t('tabs.costs')}</TabsTrigger>
2151
2151
  {!isHourlyType ? (
@@ -2159,6 +2159,14 @@ export function CollaboratorFormScreen({
2159
2159
  {!isCreateMode ? (
2160
2160
  <TabsTrigger value="salary">{t('tabs.salary')}</TabsTrigger>
2161
2161
  ) : null}
2162
+ {!isCreateMode ? (
2163
+ <TabsTrigger value="paymentHistory">
2164
+ {t('tabs.paymentHistory')}
2165
+ </TabsTrigger>
2166
+ ) : null}
2167
+ {!isCreateMode ? (
2168
+ <TabsTrigger value="invoices">{t('tabs.invoices')}</TabsTrigger>
2169
+ ) : null}
2162
2170
  </TabsList>
2163
2171
  <TabsContent value="details" className="mt-0 space-y-4 px-4 pt-2">
2164
2172
  {profileContent}
@@ -2376,6 +2384,34 @@ export function CollaboratorFormScreen({
2376
2384
  )}
2377
2385
  </TabsContent>
2378
2386
  ) : null}
2387
+ {!isCreateMode ? (
2388
+ <TabsContent value="paymentHistory" className="mt-0 px-4 pt-2">
2389
+ {collaborator ? (
2390
+ <CollaboratorPaymentHistoryTab
2391
+ collaboratorId={collaborator.id}
2392
+ disabled={!access.isDirector}
2393
+ />
2394
+ ) : (
2395
+ <p className="text-sm text-muted-foreground">
2396
+ {t('sections.costsSaveBefore')}
2397
+ </p>
2398
+ )}
2399
+ </TabsContent>
2400
+ ) : null}
2401
+ {!isCreateMode ? (
2402
+ <TabsContent value="invoices" className="mt-0 px-4 pt-2">
2403
+ {collaborator ? (
2404
+ <CollaboratorInvoicesTab
2405
+ collaboratorId={collaborator.id}
2406
+ disabled={!access.isDirector}
2407
+ />
2408
+ ) : (
2409
+ <p className="text-sm text-muted-foreground">
2410
+ {t('sections.costsSaveBefore')}
2411
+ </p>
2412
+ )}
2413
+ </TabsContent>
2414
+ ) : null}
2379
2415
  </Tabs>
2380
2416
  ) : (
2381
2417
  <div className="space-y-4 px-4">
@@ -0,0 +1,443 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Input } from '@/components/ui/input';
6
+ import { Label } from '@/components/ui/label';
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '@/components/ui/select';
14
+ import {
15
+ Table,
16
+ TableBody,
17
+ TableCell,
18
+ TableHead,
19
+ TableHeader,
20
+ TableRow,
21
+ } from '@/components/ui/table';
22
+ import { Textarea } from '@/components/ui/textarea';
23
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
24
+ import { FileTextIcon, Pencil, Plus, Trash2, X } from 'lucide-react';
25
+ import { useTranslations } from 'next-intl';
26
+ import { useState } from 'react';
27
+ import {
28
+ type CollaboratorInvoice,
29
+ createCollaboratorInvoice,
30
+ deleteCollaboratorInvoice,
31
+ fetchCollaboratorInvoices,
32
+ updateCollaboratorInvoice,
33
+ } from '../_lib/api';
34
+ import { formatCurrency, formatDate } from '../_lib/utils/format';
35
+
36
+ const INVOICE_STATUSES = ['pending', 'paid', 'cancelled', 'overdue'] as const;
37
+ type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
38
+
39
+ const STATUS_VARIANT: Record<
40
+ InvoiceStatus,
41
+ 'default' | 'secondary' | 'destructive' | 'outline'
42
+ > = {
43
+ pending: 'secondary',
44
+ paid: 'default',
45
+ cancelled: 'outline',
46
+ overdue: 'destructive',
47
+ };
48
+
49
+ type FormState = {
50
+ invoiceNumber: string;
51
+ amount: string;
52
+ issueDate: string;
53
+ dueDate: string;
54
+ status: InvoiceStatus;
55
+ description: string;
56
+ };
57
+
58
+ function emptyForm(): FormState {
59
+ return {
60
+ invoiceNumber: '',
61
+ amount: '',
62
+ issueDate: '',
63
+ dueDate: '',
64
+ status: 'pending',
65
+ description: '',
66
+ };
67
+ }
68
+
69
+ function invoiceToForm(inv: CollaboratorInvoice): FormState {
70
+ return {
71
+ invoiceNumber: inv.invoiceNumber ?? '',
72
+ amount: inv.amount ? Number(inv.amount).toFixed(2).replace('.', ',') : '',
73
+ issueDate: inv.issueDate ? inv.issueDate.slice(0, 10) : '',
74
+ dueDate: inv.dueDate ? inv.dueDate.slice(0, 10) : '',
75
+ status: (inv.status as InvoiceStatus) ?? 'pending',
76
+ description: inv.description ?? '',
77
+ };
78
+ }
79
+
80
+ type Props = {
81
+ collaboratorId: number;
82
+ disabled?: boolean;
83
+ };
84
+
85
+ export function CollaboratorInvoicesTab({
86
+ collaboratorId,
87
+ disabled = false,
88
+ }: Props) {
89
+ const t = useTranslations('operations.CollaboratorFormPage');
90
+ const commonT = useTranslations('operations.Common');
91
+ const { request, showToastHandler, getSettingValue, currentLocaleCode } =
92
+ useApp();
93
+
94
+ const [isAdding, setIsAdding] = useState(false);
95
+ const [editingId, setEditingId] = useState<number | null>(null);
96
+ const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null);
97
+ const [form, setForm] = useState<FormState>(emptyForm());
98
+ const [saving, setSaving] = useState(false);
99
+
100
+ const {
101
+ data: invoices = [],
102
+ isLoading,
103
+ refetch,
104
+ } = useQuery<CollaboratorInvoice[]>({
105
+ queryKey: [
106
+ 'operations-collaborator-invoices',
107
+ currentLocaleCode,
108
+ collaboratorId,
109
+ ],
110
+ staleTime: 0,
111
+ refetchOnMount: 'always',
112
+ queryFn: () => fetchCollaboratorInvoices(request, collaboratorId),
113
+ });
114
+
115
+ function openAdd() {
116
+ setEditingId(null);
117
+ setForm(emptyForm());
118
+ setIsAdding(true);
119
+ }
120
+
121
+ function openEdit(inv: CollaboratorInvoice) {
122
+ setIsAdding(false);
123
+ setForm(invoiceToForm(inv));
124
+ setEditingId(inv.id);
125
+ }
126
+
127
+ function cancelForm() {
128
+ setIsAdding(false);
129
+ setEditingId(null);
130
+ setForm(emptyForm());
131
+ }
132
+
133
+ async function handleSave() {
134
+ const amount = parseFloat(form.amount.replace(',', '.'));
135
+ if (!form.issueDate || isNaN(amount) || amount <= 0) return;
136
+
137
+ setSaving(true);
138
+ try {
139
+ const data = {
140
+ invoiceNumber: form.invoiceNumber || null,
141
+ amount,
142
+ issueDate: form.issueDate,
143
+ dueDate: form.dueDate || null,
144
+ status: form.status,
145
+ description: form.description || null,
146
+ };
147
+
148
+ if (editingId !== null) {
149
+ await updateCollaboratorInvoice(
150
+ request,
151
+ collaboratorId,
152
+ editingId,
153
+ data
154
+ );
155
+ } else {
156
+ await createCollaboratorInvoice(request, collaboratorId, data);
157
+ }
158
+
159
+ showToastHandler?.('success', t('messages.saveSuccess'));
160
+ cancelForm();
161
+ await refetch();
162
+ } catch {
163
+ showToastHandler?.('error', t('messages.saveError'));
164
+ } finally {
165
+ setSaving(false);
166
+ }
167
+ }
168
+
169
+ async function handleDelete(invoiceId: number) {
170
+ setSaving(true);
171
+ try {
172
+ await deleteCollaboratorInvoice(request, collaboratorId, invoiceId);
173
+ showToastHandler?.('success', t('messages.deleteSuccess'));
174
+ setConfirmDeleteId(null);
175
+ await refetch();
176
+ } catch {
177
+ showToastHandler?.('error', t('messages.deleteError'));
178
+ } finally {
179
+ setSaving(false);
180
+ }
181
+ }
182
+
183
+ const statusLabel = (s: string) => {
184
+ const key =
185
+ `invoices.status${s.charAt(0).toUpperCase() + s.slice(1)}` as Parameters<
186
+ typeof t
187
+ >[0];
188
+ try {
189
+ return t(key);
190
+ } catch {
191
+ return s;
192
+ }
193
+ };
194
+
195
+ const showForm = isAdding || editingId !== null;
196
+
197
+ return (
198
+ <div className="space-y-4">
199
+ {!disabled && (
200
+ <div className="flex justify-end">
201
+ <Button
202
+ size="sm"
203
+ variant="outline"
204
+ onClick={openAdd}
205
+ disabled={showForm}
206
+ >
207
+ <Plus className="mr-1 size-4" />
208
+ {t('invoices.add')}
209
+ </Button>
210
+ </div>
211
+ )}
212
+
213
+ {showForm && (
214
+ <div className="rounded-md border bg-muted/30 p-4">
215
+ <p className="mb-3 text-sm font-medium">
216
+ {editingId !== null ? t('invoices.editTitle') : t('invoices.add')}
217
+ </p>
218
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
219
+ <div className="space-y-1">
220
+ <Label className="text-xs">{t('invoices.invoiceNumber')}</Label>
221
+ <Input
222
+ value={form.invoiceNumber}
223
+ onChange={(e) =>
224
+ setForm((f) => ({ ...f, invoiceNumber: e.target.value }))
225
+ }
226
+ />
227
+ </div>
228
+ <div className="space-y-1">
229
+ <Label className="text-xs">{t('invoices.amount')} *</Label>
230
+ <Input
231
+ type="text"
232
+ inputMode="numeric"
233
+ placeholder="0,00"
234
+ value={form.amount}
235
+ onChange={(e) => {
236
+ const digits = e.target.value.replace(/\D/g, '');
237
+ if (!digits) {
238
+ setForm((f) => ({ ...f, amount: '' }));
239
+ return;
240
+ }
241
+ const cents = parseInt(digits, 10);
242
+ const formatted = (cents / 100).toFixed(2).replace('.', ',');
243
+ setForm((f) => ({ ...f, amount: formatted }));
244
+ }}
245
+ />
246
+ </div>
247
+ <div className="space-y-1">
248
+ <Label className="text-xs">{t('invoices.issueDate')} *</Label>
249
+ <Input
250
+ type="date"
251
+ value={form.issueDate}
252
+ onChange={(e) =>
253
+ setForm((f) => ({ ...f, issueDate: e.target.value }))
254
+ }
255
+ />
256
+ </div>
257
+ <div className="space-y-1">
258
+ <Label className="text-xs">{t('invoices.dueDate')}</Label>
259
+ <Input
260
+ type="date"
261
+ value={form.dueDate}
262
+ onChange={(e) =>
263
+ setForm((f) => ({ ...f, dueDate: e.target.value }))
264
+ }
265
+ />
266
+ </div>
267
+ <div className="space-y-1 col-span-2">
268
+ <Label className="text-xs">{t('invoices.status')}</Label>
269
+ <Select
270
+ value={form.status}
271
+ onValueChange={(v) =>
272
+ setForm((f) => ({ ...f, status: v as InvoiceStatus }))
273
+ }
274
+ >
275
+ <SelectTrigger className="w-full cursor-pointer">
276
+ <SelectValue />
277
+ </SelectTrigger>
278
+ <SelectContent>
279
+ {INVOICE_STATUSES.map((s) => (
280
+ <SelectItem key={s} value={s} className="cursor-pointer">
281
+ {statusLabel(s)}
282
+ </SelectItem>
283
+ ))}
284
+ </SelectContent>
285
+ </Select>
286
+ </div>
287
+ <div className="space-y-1 sm:col-span-2">
288
+ <Label className="text-xs">{t('invoices.description')}</Label>
289
+ <Textarea
290
+ rows={2}
291
+ value={form.description}
292
+ onChange={(e) =>
293
+ setForm((f) => ({ ...f, description: e.target.value }))
294
+ }
295
+ />
296
+ </div>
297
+ </div>
298
+ <div className="mt-3 flex justify-end gap-2">
299
+ <Button
300
+ size="sm"
301
+ variant="ghost"
302
+ onClick={cancelForm}
303
+ disabled={saving}
304
+ >
305
+ <X className="mr-1 size-4" />
306
+ {commonT('actions.cancel')}
307
+ </Button>
308
+ <Button size="sm" onClick={handleSave} disabled={saving}>
309
+ {commonT('actions.save')}
310
+ </Button>
311
+ </div>
312
+ </div>
313
+ )}
314
+
315
+ {isLoading ? (
316
+ <p className="text-sm text-muted-foreground">{t('loading')}</p>
317
+ ) : invoices.length === 0 && !showForm ? (
318
+ <div className="flex flex-col items-center gap-3 py-10 text-center">
319
+ <FileTextIcon className="size-10 text-muted-foreground/40" />
320
+ <p className="text-sm text-muted-foreground">
321
+ {t('tabs.invoicesEmpty')}
322
+ </p>
323
+ </div>
324
+ ) : invoices.length > 0 ? (
325
+ <div className="overflow-x-auto rounded-md border">
326
+ <Table>
327
+ <TableHeader>
328
+ <TableRow>
329
+ <TableHead>{t('invoices.invoiceNumber')}</TableHead>
330
+ <TableHead>{t('invoices.amount')}</TableHead>
331
+ <TableHead>{t('invoices.issueDate')}</TableHead>
332
+ <TableHead>{t('invoices.dueDate')}</TableHead>
333
+ <TableHead>{t('invoices.status')}</TableHead>
334
+ <TableHead>{t('invoices.description')}</TableHead>
335
+ {!disabled && (
336
+ <TableHead className="w-20 text-right"></TableHead>
337
+ )}
338
+ </TableRow>
339
+ </TableHeader>
340
+ <TableBody>
341
+ {invoices.map((invoice) => (
342
+ <TableRow key={invoice.id}>
343
+ <TableCell className="font-medium">
344
+ {invoice.invoiceNumber ?? '—'}
345
+ </TableCell>
346
+ <TableCell>
347
+ {formatCurrency(
348
+ Number(invoice.amount),
349
+ getSettingValue,
350
+ currentLocaleCode
351
+ )}
352
+ </TableCell>
353
+ <TableCell>
354
+ {invoice.issueDate
355
+ ? formatDate(
356
+ invoice.issueDate,
357
+ getSettingValue,
358
+ currentLocaleCode
359
+ )
360
+ : '—'}
361
+ </TableCell>
362
+ <TableCell>
363
+ {invoice.dueDate
364
+ ? formatDate(
365
+ invoice.dueDate,
366
+ getSettingValue,
367
+ currentLocaleCode
368
+ )
369
+ : '—'}
370
+ </TableCell>
371
+ <TableCell>
372
+ {invoice.status ? (
373
+ <Badge
374
+ variant={
375
+ STATUS_VARIANT[invoice.status as InvoiceStatus] ??
376
+ 'secondary'
377
+ }
378
+ >
379
+ {statusLabel(invoice.status)}
380
+ </Badge>
381
+ ) : (
382
+ '—'
383
+ )}
384
+ </TableCell>
385
+ <TableCell className="text-muted-foreground">
386
+ {invoice.description ?? '—'}
387
+ </TableCell>
388
+ {!disabled && (
389
+ <TableCell className="text-right">
390
+ {confirmDeleteId === invoice.id ? (
391
+ <div className="flex items-center justify-end gap-1">
392
+ <span className="text-xs text-muted-foreground">
393
+ {t('invoices.deleteConfirm')}
394
+ </span>
395
+ <Button
396
+ size="sm"
397
+ variant="destructive"
398
+ className="h-7 px-2 text-xs cursor-pointer"
399
+ disabled={saving}
400
+ onClick={() => handleDelete(invoice.id)}
401
+ >
402
+ {commonT('actions.delete')}
403
+ </Button>
404
+ <Button
405
+ size="sm"
406
+ variant="ghost"
407
+ className="h-7 px-2 text-xs cursor-pointer"
408
+ onClick={() => setConfirmDeleteId(null)}
409
+ >
410
+ <X className="size-3" />
411
+ </Button>
412
+ </div>
413
+ ) : (
414
+ <div className="flex items-center justify-end gap-1">
415
+ <Button
416
+ size="icon"
417
+ variant="ghost"
418
+ className="size-7 cursor-pointer"
419
+ onClick={() => openEdit(invoice)}
420
+ >
421
+ <Pencil className="size-3.5" />
422
+ </Button>
423
+ <Button
424
+ size="icon"
425
+ variant="ghost"
426
+ className="size-7 cursor-pointer text-destructive hover:text-destructive"
427
+ onClick={() => setConfirmDeleteId(invoice.id)}
428
+ >
429
+ <Trash2 className="size-3.5" />
430
+ </Button>
431
+ </div>
432
+ )}
433
+ </TableCell>
434
+ )}
435
+ </TableRow>
436
+ ))}
437
+ </TableBody>
438
+ </Table>
439
+ </div>
440
+ ) : null}
441
+ </div>
442
+ );
443
+ }