@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.
- package/dist/controllers/operations-collaborators.controller.d.ts +73 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +100 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-contracts.controller.d.ts +15 -15
- package/dist/controllers/operations-projects.controller.d.ts +3 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
- package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-invoice.dto.js +55 -0
- package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.js +50 -0
- package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.js +8 -0
- package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.js +8 -0
- package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.js +9 -0
- package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.js +9 -0
- package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
- package/dist/operations.service.d.ts +98 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +226 -3
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +32 -11
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +38 -0
- package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
- package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +212 -10
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +668 -11
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +182 -28
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +28 -7
- package/hedhog/frontend/app/_lib/api.ts.ejs +151 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
- package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
- package/hedhog/frontend/messages/en.json +96 -2
- package/hedhog/frontend/messages/pt.json +96 -2
- package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
- package/hedhog/table/operations_collaborator_payment.yaml +32 -0
- package/package.json +4 -4
- package/src/controllers/operations-collaborators.controller.ts +109 -0
- package/src/dto/create-collaborator-invoice.dto.ts +39 -0
- package/src/dto/create-collaborator-payment.dto.ts +35 -0
- package/src/dto/list-collaborator-invoice.dto.ts +3 -0
- package/src/dto/list-collaborator-payment.dto.ts +3 -0
- package/src/dto/update-collaborator-invoice.dto.ts +6 -0
- package/src/dto/update-collaborator-payment.dto.ts +6 -0
- package/src/operations.service.ts +318 -4
package/hedhog/data/menu.yaml
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
353
|
+
order: 143
|
|
333
354
|
relations:
|
|
334
355
|
role:
|
|
335
356
|
- where:
|
package/hedhog/data/route.yaml
CHANGED
|
@@ -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
|
+
}
|