@hed-hog/operations 0.0.338 → 0.0.349

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 (61) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +73 -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/controllers/operations-contracts.controller.d.ts +15 -15
  6. package/dist/controllers/operations-projects.controller.d.ts +3 -0
  7. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  8. package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
  9. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
  10. package/dist/dto/create-collaborator-invoice.dto.js +55 -0
  11. package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
  12. package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
  13. package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
  14. package/dist/dto/create-collaborator-payment.dto.js +50 -0
  15. package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
  16. package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
  17. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
  18. package/dist/dto/list-collaborator-invoice.dto.js +8 -0
  19. package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
  20. package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
  21. package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
  22. package/dist/dto/list-collaborator-payment.dto.js +8 -0
  23. package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
  24. package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
  25. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
  26. package/dist/dto/update-collaborator-invoice.dto.js +9 -0
  27. package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
  28. package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
  29. package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
  30. package/dist/dto/update-collaborator-payment.dto.js +9 -0
  31. package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
  32. package/dist/operations.service.d.ts +98 -0
  33. package/dist/operations.service.d.ts.map +1 -1
  34. package/dist/operations.service.js +226 -3
  35. package/dist/operations.service.js.map +1 -1
  36. package/hedhog/data/menu.yaml +32 -11
  37. package/hedhog/data/route.yaml +72 -0
  38. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +38 -0
  39. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
  40. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
  41. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +212 -10
  42. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +668 -11
  43. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +182 -28
  44. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +28 -7
  45. package/hedhog/frontend/app/_lib/api.ts.ejs +151 -0
  46. package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
  47. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
  48. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
  49. package/hedhog/frontend/messages/en.json +96 -2
  50. package/hedhog/frontend/messages/pt.json +96 -2
  51. package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
  52. package/hedhog/table/operations_collaborator_payment.yaml +32 -0
  53. package/package.json +4 -4
  54. package/src/controllers/operations-collaborators.controller.ts +109 -0
  55. package/src/dto/create-collaborator-invoice.dto.ts +39 -0
  56. package/src/dto/create-collaborator-payment.dto.ts +35 -0
  57. package/src/dto/list-collaborator-invoice.dto.ts +3 -0
  58. package/src/dto/list-collaborator-payment.dto.ts +3 -0
  59. package/src/dto/update-collaborator-invoice.dto.ts +6 -0
  60. package/src/dto/update-collaborator-payment.dto.ts +6 -0
  61. package/src/operations.service.ts +318 -4
@@ -134,6 +134,27 @@
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: operations-collaborator
153
+ - where:
154
+ slug: admin-operations-supervisor
155
+ - where:
156
+ slug: admin-operations-director
157
+
137
158
  - menu_id:
138
159
  where:
139
160
  slug: /operations
@@ -143,7 +164,7 @@
143
164
  en: Approvals
144
165
  pt: Aprovações
145
166
  slug: /operations/approvals
146
- order: 118
167
+ order: 119
147
168
  relations:
148
169
  role:
149
170
  - where:
@@ -162,7 +183,7 @@
162
183
  en: Time Off
163
184
  pt: Folgas
164
185
  slug: /operations/time-off
165
- order: 119
186
+ order: 120
166
187
  relations:
167
188
  role:
168
189
  - where:
@@ -183,7 +204,7 @@
183
204
  en: Schedule Adjustments
184
205
  pt: Ajustes de Jornada
185
206
  slug: /operations/schedule-adjustments
186
- order: 120
207
+ order: 121
187
208
  relations:
188
209
  role:
189
210
  - where:
@@ -204,7 +225,7 @@
204
225
  en: Departments
205
226
  pt: Departamentos
206
227
  slug: /operations/departments
207
- order: 121
228
+ order: 122
208
229
  relations:
209
230
  role:
210
231
  - where:
@@ -221,7 +242,7 @@
221
242
  en: Collaborator Types
222
243
  pt: Tipos de Vinculo
223
244
  slug: /operations/collaborator-types
224
- order: 122
245
+ order: 123
225
246
  relations:
226
247
  role:
227
248
  - where:
@@ -237,7 +258,7 @@
237
258
  en: Reports
238
259
  pt: Relatórios
239
260
  slug: /operations/reports
240
- order: 123
261
+ order: 124
241
262
  relations:
242
263
  role:
243
264
  - where:
@@ -256,7 +277,7 @@
256
277
  en: Projects
257
278
  pt: Projetos
258
279
  slug: /operations/reports/projects
259
- order: 124
280
+ order: 125
260
281
  relations:
261
282
  role:
262
283
  - where:
@@ -275,7 +296,7 @@
275
296
  en: Collaborators
276
297
  pt: Colaboradores
277
298
  slug: /operations/reports/collaborators
278
- order: 125
299
+ order: 126
279
300
  relations:
280
301
  role:
281
302
  - where:
@@ -293,7 +314,7 @@
293
314
  en: Management
294
315
  pt: Gerenciamento
295
316
  slug: /operations/management
296
- order: 140
317
+ order: 141
297
318
  relations:
298
319
  role:
299
320
  - where:
@@ -312,7 +333,7 @@
312
333
  en: Cost Categories
313
334
  pt: Categorias de Custo
314
335
  slug: /operations/project-cost-categories
315
- order: 141
336
+ order: 142
316
337
  relations:
317
338
  role:
318
339
  - where:
@@ -329,7 +350,7 @@
329
350
  en: Cost Types
330
351
  pt: Tipos de Custo
331
352
  slug: /operations/project-cost-types
332
- order: 142
353
+ order: 143
333
354
  relations:
334
355
  role:
335
356
  - 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/invoices
114
+ method: GET
115
+ relations:
116
+ role:
117
+ - where:
118
+ slug: admin
119
+ - where:
120
+ slug: admin-operations-director
121
+
122
+ - url: /operations/collaborators/:id/payment-history
123
+ method: POST
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: PATCH
133
+ relations:
134
+ role:
135
+ - where:
136
+ slug: admin
137
+ - where:
138
+ slug: admin-operations-director
139
+
140
+ - url: /operations/collaborators/:id/payment-history/:paymentId
141
+ method: DELETE
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';
@@ -2159,6 +2161,14 @@ export function CollaboratorFormScreen({
2159
2161
  {!isCreateMode ? (
2160
2162
  <TabsTrigger value="salary">{t('tabs.salary')}</TabsTrigger>
2161
2163
  ) : null}
2164
+ {!isCreateMode ? (
2165
+ <TabsTrigger value="paymentHistory">
2166
+ {t('tabs.paymentHistory')}
2167
+ </TabsTrigger>
2168
+ ) : null}
2169
+ {!isCreateMode ? (
2170
+ <TabsTrigger value="invoices">{t('tabs.invoices')}</TabsTrigger>
2171
+ ) : null}
2162
2172
  </TabsList>
2163
2173
  <TabsContent value="details" className="mt-0 space-y-4 px-4 pt-2">
2164
2174
  {profileContent}
@@ -2376,6 +2386,34 @@ export function CollaboratorFormScreen({
2376
2386
  )}
2377
2387
  </TabsContent>
2378
2388
  ) : null}
2389
+ {!isCreateMode ? (
2390
+ <TabsContent value="paymentHistory" className="mt-0 px-4 pt-2">
2391
+ {collaborator ? (
2392
+ <CollaboratorPaymentHistoryTab
2393
+ collaboratorId={collaborator.id}
2394
+ disabled={!access.isDirector}
2395
+ />
2396
+ ) : (
2397
+ <p className="text-sm text-muted-foreground">
2398
+ {t('sections.costsSaveBefore')}
2399
+ </p>
2400
+ )}
2401
+ </TabsContent>
2402
+ ) : null}
2403
+ {!isCreateMode ? (
2404
+ <TabsContent value="invoices" className="mt-0 px-4 pt-2">
2405
+ {collaborator ? (
2406
+ <CollaboratorInvoicesTab
2407
+ collaboratorId={collaborator.id}
2408
+ disabled={!access.isDirector}
2409
+ />
2410
+ ) : (
2411
+ <p className="text-sm text-muted-foreground">
2412
+ {t('sections.costsSaveBefore')}
2413
+ </p>
2414
+ )}
2415
+ </TabsContent>
2416
+ ) : null}
2379
2417
  </Tabs>
2380
2418
  ) : (
2381
2419
  <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
+ }