@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.
- package/dist/controllers/operations-collaborators.controller.d.ts +54 -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/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 +76 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +235 -5
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +27 -8
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -3
- 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/my-project-summary-screen.tsx.ejs +86 -87
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +218 -10
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +710 -26
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +158 -38
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +807 -803
- package/hedhog/frontend/app/_lib/api.ts.ejs +631 -480
- package/hedhog/frontend/app/_lib/types.ts.ejs +6 -5
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +16 -2
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +95 -157
- package/hedhog/frontend/app/projects/page.tsx.ejs +42 -6
- 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 +5 -5
- package/src/controllers/operations-collaborators.controller.ts +117 -8
- 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 +328 -5
package/hedhog/data/menu.yaml
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
297
|
+
order: 126
|
|
279
298
|
relations:
|
|
280
299
|
role:
|
|
281
300
|
- 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/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
|
+
}
|