@hed-hog/operations 0.0.296 → 0.0.298

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 (32) hide show
  1. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -310
  2. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -631
  3. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -132
  4. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -558
  5. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -291
  6. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -689
  7. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -32
  8. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -44
  9. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -360
  10. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -129
  11. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -14
  12. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -386
  13. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -11
  14. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -11
  15. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -5
  16. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -261
  17. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -11
  18. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -11
  19. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -17
  20. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -262
  21. package/hedhog/frontend/app/page.tsx.ejs +319 -319
  22. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -11
  23. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -11
  24. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -5
  25. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -236
  26. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -418
  27. package/hedhog/frontend/app/team/page.tsx.ejs +339 -339
  28. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -328
  29. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -636
  30. package/hedhog/frontend/messages/en.json +648 -648
  31. package/hedhog/frontend/messages/pt.json +647 -647
  32. package/package.json +2 -2
@@ -1,17 +1,17 @@
1
- import { ContractFormScreen } from '../../_components/contract-form-screen';
2
-
3
- export default async function OperationsContractCreatePage({
4
- searchParams,
5
- }: {
6
- searchParams?: Promise<{ duplicate?: string }>;
7
- }) {
8
- const resolved = searchParams ? await searchParams : undefined;
9
-
10
- return (
11
- <ContractFormScreen
12
- duplicateFromId={
13
- resolved?.duplicate ? Number(resolved.duplicate) : undefined
14
- }
15
- />
16
- );
17
- }
1
+ import { ContractFormScreen } from '../../_components/contract-form-screen';
2
+
3
+ export default async function OperationsContractCreatePage({
4
+ searchParams,
5
+ }: {
6
+ searchParams?: Promise<{ duplicate?: string }>;
7
+ }) {
8
+ const resolved = searchParams ? await searchParams : undefined;
9
+
10
+ return (
11
+ <ContractFormScreen
12
+ duplicateFromId={
13
+ resolved?.duplicate ? Number(resolved.duplicate) : undefined
14
+ }
15
+ />
16
+ );
17
+ }
@@ -1,262 +1,262 @@
1
- 'use client';
2
-
3
- import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
- import { Button } from '@/components/ui/button';
5
- import {
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHead,
10
- TableHeader,
11
- TableRow,
12
- } from '@/components/ui/table';
13
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
14
- import { Download, Eye, FileText, Pencil, Upload } from 'lucide-react';
15
- import Link from 'next/link';
16
- import { useMemo, useRef, useState } from 'react';
17
- import { useTranslations } from 'next-intl';
18
- import { OperationsHeader } from '../_components/operations-header';
19
- import { StatusBadge } from '../_components/status-badge';
20
- import { fetchOperations, mutateOperations } from '../_lib/api';
21
- import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
22
- import type { OperationsContract, OperationsContractDetails } from '../_lib/types';
23
- import {
24
- formatCurrency,
25
- formatDate,
26
- formatEnumLabel,
27
- getStatusBadgeClass,
28
- } from '../_lib/utils/format';
29
-
30
- function downloadBase64File(fileName: string, mimeType: string, base64: string) {
31
- const href = `data:${mimeType};base64,${base64}`;
32
- const link = document.createElement('a');
33
- link.href = href;
34
- link.download = fileName;
35
- link.click();
36
- }
37
-
38
- async function fileToBase64(file: File) {
39
- return new Promise<string>((resolve, reject) => {
40
- const reader = new FileReader();
41
- reader.onload = () => {
42
- const result = String(reader.result ?? '');
43
- const [, base64 = ''] = result.split(',');
44
- resolve(base64);
45
- };
46
- reader.onerror = reject;
47
- reader.readAsDataURL(file);
48
- });
49
- }
50
-
51
- export default function OperationsContractsPage() {
52
- const t = useTranslations('operations.ContractsPage');
53
- const commonT = useTranslations('operations.Common');
54
- const { request, showToastHandler, currentLocaleCode } = useApp();
55
- const access = useOperationsAccess();
56
- const [search, setSearch] = useState('');
57
- const [statusFilter, setStatusFilter] = useState('all');
58
- const [categoryFilter, setCategoryFilter] = useState('all');
59
- const [originFilter, setOriginFilter] = useState('all');
60
- const uploadTargetRef = useRef<number | null>(null);
61
- const fileInputRef = useRef<HTMLInputElement>(null);
62
-
63
- const { data: contracts = [], refetch } = useQuery<OperationsContract[]>({
64
- queryKey: ['operations-contracts-list', currentLocaleCode],
65
- queryFn: () => fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
66
- });
67
-
68
- const filteredRows = useMemo(
69
- () =>
70
- contracts.filter((item) => {
71
- const matchesSearch = !search.trim()
72
- ? true
73
- : [
74
- item.name,
75
- item.code,
76
- item.mainRelatedPartyName,
77
- item.clientName,
78
- item.contractType,
79
- ]
80
- .filter(Boolean)
81
- .some((value) =>
82
- String(value).toLowerCase().includes(search.trim().toLowerCase())
83
- );
84
- const matchesStatus = statusFilter === 'all' ? true : item.status === statusFilter;
85
- const matchesCategory =
86
- categoryFilter === 'all' ? true : item.contractCategory === categoryFilter;
87
- const matchesOrigin =
88
- originFilter === 'all' ? true : item.originType === originFilter;
89
- return matchesSearch && matchesStatus && matchesCategory && matchesOrigin;
90
- }),
91
- [contracts, search, statusFilter, categoryFilter, originFilter]
92
- );
93
-
94
- const downloadPdf = async (contractId: number) => {
95
- const detail = await fetchOperations<OperationsContractDetails>(
96
- request,
97
- `/operations/contracts/${contractId}`
98
- );
99
- const document = detail.documents.find((item) => item.isCurrent && item.fileContentBase64);
100
- if (!document?.fileContentBase64) {
101
- showToastHandler?.('error', t('messages.noPdf'));
102
- return;
103
- }
104
- downloadBase64File(document.fileName, document.mimeType, document.fileContentBase64);
105
- };
106
-
107
- const duplicateContract = async (contractId: number) => {
108
- window.location.href = `/operations/contracts/new?duplicate=${contractId}`;
109
- };
110
-
111
- const toggleArchived = async (contract: OperationsContract) => {
112
- try {
113
- await mutateOperations(request, `/operations/contracts/${contract.id}`, 'PATCH', {
114
- status: contract.status === 'archived' ? 'active' : 'archived',
115
- isActive: contract.status === 'archived',
116
- });
117
- showToastHandler?.('success', t('messages.statusSuccess'));
118
- await refetch();
119
- } catch {
120
- showToastHandler?.('error', t('messages.statusError'));
121
- }
122
- };
123
-
124
- return (
125
- <Page>
126
- <OperationsHeader
127
- title={t('title')}
128
- description={t('description')}
129
- current={t('breadcrumb')}
130
- actions={
131
- access.isDirector ? (
132
- <Button size="sm" asChild>
133
- <Link href="/operations/contracts/new">{commonT('actions.create')}</Link>
134
- </Button>
135
- ) : undefined
136
- }
137
- />
138
-
139
- <SearchBar
140
- searchQuery={search}
141
- onSearchChange={setSearch}
142
- onSearch={() => undefined}
143
- placeholder={t('searchPlaceholder')}
144
- controls={[
145
- {
146
- id: 'category',
147
- type: 'select',
148
- value: categoryFilter,
149
- onChange: setCategoryFilter,
150
- placeholder: commonT('labels.contractCategory'),
151
- options: [{ value: 'all', label: commonT('filters.allTypes') }, ...['employee','contractor','client','supplier','vendor','partner','internal','other'].map((value) => ({ value, label: formatEnumLabel(value) }))],
152
- },
153
- {
154
- id: 'origin',
155
- type: 'select',
156
- value: originFilter,
157
- onChange: setOriginFilter,
158
- placeholder: t('filters.originType'),
159
- options: [{ value: 'all', label: commonT('filters.allTypes') }, ...['manual','employee_hiring','client_project'].map((value) => ({ value, label: formatEnumLabel(value) }))],
160
- },
161
- {
162
- id: 'status',
163
- type: 'select',
164
- value: statusFilter,
165
- onChange: setStatusFilter,
166
- placeholder: commonT('labels.status'),
167
- options: [{ value: 'all', label: commonT('filters.allStatuses') }, ...['draft','under_review','active','renewal','expired','closed','archived'].map((value) => ({ value, label: formatEnumLabel(value) }))],
168
- },
169
- ]}
170
- />
171
-
172
- <input
173
- ref={fileInputRef}
174
- type="file"
175
- accept="application/pdf"
176
- className="hidden"
177
- onChange={async (event) => {
178
- const file = event.target.files?.[0];
179
- const contractId = uploadTargetRef.current;
180
- if (!file || !contractId) return;
181
- try {
182
- const fileContentBase64 = await fileToBase64(file);
183
- await mutateOperations(request, `/operations/contracts/${contractId}`, 'PATCH', {
184
- replaceUploadedPdfDocument: {
185
- fileName: file.name,
186
- mimeType: file.type || 'application/pdf',
187
- fileContentBase64,
188
- },
189
- });
190
- showToastHandler?.('success', t('messages.uploadSuccess'));
191
- await refetch();
192
- } catch {
193
- showToastHandler?.('error', t('messages.uploadError'));
194
- } finally {
195
- uploadTargetRef.current = null;
196
- event.target.value = '';
197
- }
198
- }}
199
- />
200
-
201
- {filteredRows.length ? (
202
- <div className="overflow-x-auto rounded-md border">
203
- <Table>
204
- <TableHeader>
205
- <TableRow>
206
- <TableHead>{t('columns.title')}</TableHead>
207
- <TableHead>{t('columns.code')}</TableHead>
208
- <TableHead>{commonT('labels.contractCategory')}</TableHead>
209
- <TableHead>{t('columns.type')}</TableHead>
210
- <TableHead>{t('columns.origin')}</TableHead>
211
- <TableHead>{t('columns.party')}</TableHead>
212
- <TableHead>{commonT('labels.startDate')}</TableHead>
213
- <TableHead>{commonT('labels.endDate')}</TableHead>
214
- <TableHead>{t('columns.signatureStatus')}</TableHead>
215
- <TableHead>{t('columns.active')}</TableHead>
216
- <TableHead>{t('columns.financials')}</TableHead>
217
- <TableHead>{commonT('labels.status')}</TableHead>
218
- <TableHead className="w-[320px] text-right">{commonT('labels.actions')}</TableHead>
219
- </TableRow>
220
- </TableHeader>
221
- <TableBody>
222
- {filteredRows.map((contract) => (
223
- <TableRow key={contract.id}>
224
- <TableCell>{contract.name}</TableCell>
225
- <TableCell>{contract.code}</TableCell>
226
- <TableCell>{formatEnumLabel(contract.contractCategory)}</TableCell>
227
- <TableCell>{formatEnumLabel(contract.contractType)}</TableCell>
228
- <TableCell>{formatEnumLabel(contract.originType)}</TableCell>
229
- <TableCell>{contract.mainRelatedPartyName || commonT('labels.notAvailable')}</TableCell>
230
- <TableCell>{formatDate(contract.startDate)}</TableCell>
231
- <TableCell>{formatDate(contract.endDate)}</TableCell>
232
- <TableCell><StatusBadge label={formatEnumLabel(contract.signatureStatus)} className={getStatusBadgeClass(contract.signatureStatus)} /></TableCell>
233
- <TableCell>{contract.isActive ? t('labels.active') : t('labels.inactive')}</TableCell>
234
- <TableCell>{formatCurrency((contract.valueAmount ?? 0) + (contract.revenueAmount ?? 0))}</TableCell>
235
- <TableCell><StatusBadge label={formatEnumLabel(contract.status)} className={getStatusBadgeClass(contract.status)} /></TableCell>
236
- <TableCell>
237
- <div className="flex justify-end gap-2">
238
- <Button variant="outline" size="icon" asChild><Link href={`/operations/contracts/${contract.id}`}><Eye className="size-4" /></Link></Button>
239
- {access.isDirector ? <Button variant="outline" size="icon" asChild><Link href={`/operations/contracts/${contract.id}/edit`}><Pencil className="size-4" /></Link></Button> : null}
240
- <Button variant="outline" size="icon" onClick={() => void downloadPdf(contract.id)}><Download className="size-4" /></Button>
241
- {access.isDirector ? <Button variant="outline" size="icon" onClick={() => { uploadTargetRef.current = contract.id; fileInputRef.current?.click(); }}><Upload className="size-4" /></Button> : null}
242
- {access.isDirector ? <Button variant="outline" size="sm" onClick={() => void duplicateContract(contract.id)}>{t('actions.duplicate')}</Button> : null}
243
- {access.isDirector ? <Button variant="outline" size="sm" onClick={() => void toggleArchived(contract)}>{contract.status === 'archived' ? commonT('actions.activate') : t('actions.archive')}</Button> : null}
244
- </div>
245
- </TableCell>
246
- </TableRow>
247
- ))}
248
- </TableBody>
249
- </Table>
250
- </div>
251
- ) : (
252
- <EmptyState
253
- icon={<FileText className="size-12" />}
254
- title={commonT('states.emptyTitle')}
255
- description={t('emptyDescription')}
256
- actionLabel={access.isDirector ? commonT('actions.create') : commonT('actions.refresh')}
257
- onAction={access.isDirector ? () => { window.location.href = '/operations/contracts/new'; } : () => void refetch()}
258
- />
259
- )}
260
- </Page>
261
- );
262
- }
1
+ 'use client';
2
+
3
+ import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from '@/components/ui/table';
13
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
14
+ import { Download, Eye, FileText, Pencil, Upload } from 'lucide-react';
15
+ import Link from 'next/link';
16
+ import { useMemo, useRef, useState } from 'react';
17
+ import { useTranslations } from 'next-intl';
18
+ import { OperationsHeader } from '../_components/operations-header';
19
+ import { StatusBadge } from '../_components/status-badge';
20
+ import { fetchOperations, mutateOperations } from '../_lib/api';
21
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
22
+ import type { OperationsContract, OperationsContractDetails } from '../_lib/types';
23
+ import {
24
+ formatCurrency,
25
+ formatDate,
26
+ formatEnumLabel,
27
+ getStatusBadgeClass,
28
+ } from '../_lib/utils/format';
29
+
30
+ function downloadBase64File(fileName: string, mimeType: string, base64: string) {
31
+ const href = `data:${mimeType};base64,${base64}`;
32
+ const link = document.createElement('a');
33
+ link.href = href;
34
+ link.download = fileName;
35
+ link.click();
36
+ }
37
+
38
+ async function fileToBase64(file: File) {
39
+ return new Promise<string>((resolve, reject) => {
40
+ const reader = new FileReader();
41
+ reader.onload = () => {
42
+ const result = String(reader.result ?? '');
43
+ const [, base64 = ''] = result.split(',');
44
+ resolve(base64);
45
+ };
46
+ reader.onerror = reject;
47
+ reader.readAsDataURL(file);
48
+ });
49
+ }
50
+
51
+ export default function OperationsContractsPage() {
52
+ const t = useTranslations('operations.ContractsPage');
53
+ const commonT = useTranslations('operations.Common');
54
+ const { request, showToastHandler, currentLocaleCode } = useApp();
55
+ const access = useOperationsAccess();
56
+ const [search, setSearch] = useState('');
57
+ const [statusFilter, setStatusFilter] = useState('all');
58
+ const [categoryFilter, setCategoryFilter] = useState('all');
59
+ const [originFilter, setOriginFilter] = useState('all');
60
+ const uploadTargetRef = useRef<number | null>(null);
61
+ const fileInputRef = useRef<HTMLInputElement>(null);
62
+
63
+ const { data: contracts = [], refetch } = useQuery<OperationsContract[]>({
64
+ queryKey: ['operations-contracts-list', currentLocaleCode],
65
+ queryFn: () => fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
66
+ });
67
+
68
+ const filteredRows = useMemo(
69
+ () =>
70
+ contracts.filter((item) => {
71
+ const matchesSearch = !search.trim()
72
+ ? true
73
+ : [
74
+ item.name,
75
+ item.code,
76
+ item.mainRelatedPartyName,
77
+ item.clientName,
78
+ item.contractType,
79
+ ]
80
+ .filter(Boolean)
81
+ .some((value) =>
82
+ String(value).toLowerCase().includes(search.trim().toLowerCase())
83
+ );
84
+ const matchesStatus = statusFilter === 'all' ? true : item.status === statusFilter;
85
+ const matchesCategory =
86
+ categoryFilter === 'all' ? true : item.contractCategory === categoryFilter;
87
+ const matchesOrigin =
88
+ originFilter === 'all' ? true : item.originType === originFilter;
89
+ return matchesSearch && matchesStatus && matchesCategory && matchesOrigin;
90
+ }),
91
+ [contracts, search, statusFilter, categoryFilter, originFilter]
92
+ );
93
+
94
+ const downloadPdf = async (contractId: number) => {
95
+ const detail = await fetchOperations<OperationsContractDetails>(
96
+ request,
97
+ `/operations/contracts/${contractId}`
98
+ );
99
+ const document = detail.documents.find((item) => item.isCurrent && item.fileContentBase64);
100
+ if (!document?.fileContentBase64) {
101
+ showToastHandler?.('error', t('messages.noPdf'));
102
+ return;
103
+ }
104
+ downloadBase64File(document.fileName, document.mimeType, document.fileContentBase64);
105
+ };
106
+
107
+ const duplicateContract = async (contractId: number) => {
108
+ window.location.href = `/operations/contracts/new?duplicate=${contractId}`;
109
+ };
110
+
111
+ const toggleArchived = async (contract: OperationsContract) => {
112
+ try {
113
+ await mutateOperations(request, `/operations/contracts/${contract.id}`, 'PATCH', {
114
+ status: contract.status === 'archived' ? 'active' : 'archived',
115
+ isActive: contract.status === 'archived',
116
+ });
117
+ showToastHandler?.('success', t('messages.statusSuccess'));
118
+ await refetch();
119
+ } catch {
120
+ showToastHandler?.('error', t('messages.statusError'));
121
+ }
122
+ };
123
+
124
+ return (
125
+ <Page>
126
+ <OperationsHeader
127
+ title={t('title')}
128
+ description={t('description')}
129
+ current={t('breadcrumb')}
130
+ actions={
131
+ access.isDirector ? (
132
+ <Button size="sm" asChild>
133
+ <Link href="/operations/contracts/new">{commonT('actions.create')}</Link>
134
+ </Button>
135
+ ) : undefined
136
+ }
137
+ />
138
+
139
+ <SearchBar
140
+ searchQuery={search}
141
+ onSearchChange={setSearch}
142
+ onSearch={() => undefined}
143
+ placeholder={t('searchPlaceholder')}
144
+ controls={[
145
+ {
146
+ id: 'category',
147
+ type: 'select',
148
+ value: categoryFilter,
149
+ onChange: setCategoryFilter,
150
+ placeholder: commonT('labels.contractCategory'),
151
+ options: [{ value: 'all', label: commonT('filters.allTypes') }, ...['employee','contractor','client','supplier','vendor','partner','internal','other'].map((value) => ({ value, label: formatEnumLabel(value) }))],
152
+ },
153
+ {
154
+ id: 'origin',
155
+ type: 'select',
156
+ value: originFilter,
157
+ onChange: setOriginFilter,
158
+ placeholder: t('filters.originType'),
159
+ options: [{ value: 'all', label: commonT('filters.allTypes') }, ...['manual','employee_hiring','client_project'].map((value) => ({ value, label: formatEnumLabel(value) }))],
160
+ },
161
+ {
162
+ id: 'status',
163
+ type: 'select',
164
+ value: statusFilter,
165
+ onChange: setStatusFilter,
166
+ placeholder: commonT('labels.status'),
167
+ options: [{ value: 'all', label: commonT('filters.allStatuses') }, ...['draft','under_review','active','renewal','expired','closed','archived'].map((value) => ({ value, label: formatEnumLabel(value) }))],
168
+ },
169
+ ]}
170
+ />
171
+
172
+ <input
173
+ ref={fileInputRef}
174
+ type="file"
175
+ accept="application/pdf"
176
+ className="hidden"
177
+ onChange={async (event) => {
178
+ const file = event.target.files?.[0];
179
+ const contractId = uploadTargetRef.current;
180
+ if (!file || !contractId) return;
181
+ try {
182
+ const fileContentBase64 = await fileToBase64(file);
183
+ await mutateOperations(request, `/operations/contracts/${contractId}`, 'PATCH', {
184
+ replaceUploadedPdfDocument: {
185
+ fileName: file.name,
186
+ mimeType: file.type || 'application/pdf',
187
+ fileContentBase64,
188
+ },
189
+ });
190
+ showToastHandler?.('success', t('messages.uploadSuccess'));
191
+ await refetch();
192
+ } catch {
193
+ showToastHandler?.('error', t('messages.uploadError'));
194
+ } finally {
195
+ uploadTargetRef.current = null;
196
+ event.target.value = '';
197
+ }
198
+ }}
199
+ />
200
+
201
+ {filteredRows.length ? (
202
+ <div className="overflow-x-auto rounded-md border">
203
+ <Table>
204
+ <TableHeader>
205
+ <TableRow>
206
+ <TableHead>{t('columns.title')}</TableHead>
207
+ <TableHead>{t('columns.code')}</TableHead>
208
+ <TableHead>{commonT('labels.contractCategory')}</TableHead>
209
+ <TableHead>{t('columns.type')}</TableHead>
210
+ <TableHead>{t('columns.origin')}</TableHead>
211
+ <TableHead>{t('columns.party')}</TableHead>
212
+ <TableHead>{commonT('labels.startDate')}</TableHead>
213
+ <TableHead>{commonT('labels.endDate')}</TableHead>
214
+ <TableHead>{t('columns.signatureStatus')}</TableHead>
215
+ <TableHead>{t('columns.active')}</TableHead>
216
+ <TableHead>{t('columns.financials')}</TableHead>
217
+ <TableHead>{commonT('labels.status')}</TableHead>
218
+ <TableHead className="w-[320px] text-right">{commonT('labels.actions')}</TableHead>
219
+ </TableRow>
220
+ </TableHeader>
221
+ <TableBody>
222
+ {filteredRows.map((contract) => (
223
+ <TableRow key={contract.id}>
224
+ <TableCell>{contract.name}</TableCell>
225
+ <TableCell>{contract.code}</TableCell>
226
+ <TableCell>{formatEnumLabel(contract.contractCategory)}</TableCell>
227
+ <TableCell>{formatEnumLabel(contract.contractType)}</TableCell>
228
+ <TableCell>{formatEnumLabel(contract.originType)}</TableCell>
229
+ <TableCell>{contract.mainRelatedPartyName || commonT('labels.notAvailable')}</TableCell>
230
+ <TableCell>{formatDate(contract.startDate)}</TableCell>
231
+ <TableCell>{formatDate(contract.endDate)}</TableCell>
232
+ <TableCell><StatusBadge label={formatEnumLabel(contract.signatureStatus)} className={getStatusBadgeClass(contract.signatureStatus)} /></TableCell>
233
+ <TableCell>{contract.isActive ? t('labels.active') : t('labels.inactive')}</TableCell>
234
+ <TableCell>{formatCurrency((contract.valueAmount ?? 0) + (contract.revenueAmount ?? 0))}</TableCell>
235
+ <TableCell><StatusBadge label={formatEnumLabel(contract.status)} className={getStatusBadgeClass(contract.status)} /></TableCell>
236
+ <TableCell>
237
+ <div className="flex justify-end gap-2">
238
+ <Button variant="outline" size="icon" asChild><Link href={`/operations/contracts/${contract.id}`}><Eye className="size-4" /></Link></Button>
239
+ {access.isDirector ? <Button variant="outline" size="icon" asChild><Link href={`/operations/contracts/${contract.id}/edit`}><Pencil className="size-4" /></Link></Button> : null}
240
+ <Button variant="outline" size="icon" onClick={() => void downloadPdf(contract.id)}><Download className="size-4" /></Button>
241
+ {access.isDirector ? <Button variant="outline" size="icon" onClick={() => { uploadTargetRef.current = contract.id; fileInputRef.current?.click(); }}><Upload className="size-4" /></Button> : null}
242
+ {access.isDirector ? <Button variant="outline" size="sm" onClick={() => void duplicateContract(contract.id)}>{t('actions.duplicate')}</Button> : null}
243
+ {access.isDirector ? <Button variant="outline" size="sm" onClick={() => void toggleArchived(contract)}>{contract.status === 'archived' ? commonT('actions.activate') : t('actions.archive')}</Button> : null}
244
+ </div>
245
+ </TableCell>
246
+ </TableRow>
247
+ ))}
248
+ </TableBody>
249
+ </Table>
250
+ </div>
251
+ ) : (
252
+ <EmptyState
253
+ icon={<FileText className="size-12" />}
254
+ title={commonT('states.emptyTitle')}
255
+ description={t('emptyDescription')}
256
+ actionLabel={access.isDirector ? commonT('actions.create') : commonT('actions.refresh')}
257
+ onAction={access.isDirector ? () => { window.location.href = '/operations/contracts/new'; } : () => void refetch()}
258
+ />
259
+ )}
260
+ </Page>
261
+ );
262
+ }