@hed-hog/operations 0.0.301 → 0.0.302
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/operations.service.d.ts +1 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +77 -37
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +1 -22
- package/hedhog/frontend/app/page.tsx.ejs +3 -341
- package/package.json +6 -6
- package/src/operations.service.ts +70 -7
package/hedhog/data/menu.yaml
CHANGED
|
@@ -16,27 +16,6 @@
|
|
|
16
16
|
- where:
|
|
17
17
|
slug: admin-operations-director
|
|
18
18
|
|
|
19
|
-
- menu_id:
|
|
20
|
-
where:
|
|
21
|
-
slug: /operations
|
|
22
|
-
icon: layout-dashboard
|
|
23
|
-
url: /operations
|
|
24
|
-
name:
|
|
25
|
-
en: Dashboard
|
|
26
|
-
pt: Dashboard
|
|
27
|
-
slug: /operations/dashboard
|
|
28
|
-
order: 111
|
|
29
|
-
relations:
|
|
30
|
-
role:
|
|
31
|
-
- where:
|
|
32
|
-
slug: admin
|
|
33
|
-
- where:
|
|
34
|
-
slug: admin-operations-collaborator
|
|
35
|
-
- where:
|
|
36
|
-
slug: admin-operations-supervisor
|
|
37
|
-
- where:
|
|
38
|
-
slug: admin-operations-director
|
|
39
|
-
|
|
40
19
|
- menu_id:
|
|
41
20
|
where:
|
|
42
21
|
slug: /operations
|
|
@@ -156,7 +135,7 @@
|
|
|
156
135
|
- menu_id:
|
|
157
136
|
where:
|
|
158
137
|
slug: /operations
|
|
159
|
-
icon:
|
|
138
|
+
icon: calendar-time
|
|
160
139
|
url: /operations/time-off
|
|
161
140
|
name:
|
|
162
141
|
en: Time Off
|
|
@@ -1,343 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
6
|
-
import { Skeleton } from '@/components/ui/skeleton';
|
|
7
|
-
import {
|
|
8
|
-
Table,
|
|
9
|
-
TableBody,
|
|
10
|
-
TableCell,
|
|
11
|
-
TableHead,
|
|
12
|
-
TableHeader,
|
|
13
|
-
TableRow,
|
|
14
|
-
} from '@/components/ui/table';
|
|
15
|
-
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
16
|
-
import {
|
|
17
|
-
Building2,
|
|
18
|
-
CheckCircle2,
|
|
19
|
-
ClipboardList,
|
|
20
|
-
FolderKanban,
|
|
21
|
-
Hourglass,
|
|
22
|
-
PlaneTakeoff,
|
|
23
|
-
RefreshCcw,
|
|
24
|
-
Users,
|
|
25
|
-
} from 'lucide-react';
|
|
26
|
-
import { useTranslations } from 'next-intl';
|
|
27
|
-
import Link from 'next/link';
|
|
28
|
-
import { OperationsHeader } from './_components/operations-header';
|
|
29
|
-
import { SectionCard } from './_components/section-card';
|
|
30
|
-
import { StatusBadge } from './_components/status-badge';
|
|
31
|
-
import { fetchOperations } from './_lib/api';
|
|
32
|
-
import { useOperationsAccess } from './_lib/hooks/use-operations-access';
|
|
33
|
-
import type { OperationsDashboard, OperationsTeamOverview } from './_lib/types';
|
|
34
|
-
import {
|
|
35
|
-
formatDateRange,
|
|
36
|
-
formatEnumLabel,
|
|
37
|
-
formatHours,
|
|
38
|
-
getStatusBadgeClass,
|
|
39
|
-
} from './_lib/utils/format';
|
|
40
|
-
|
|
41
|
-
export default function OperationsDashboardPage() {
|
|
42
|
-
const t = useTranslations('operations.DashboardPage');
|
|
43
|
-
const commonT = useTranslations('operations.Common');
|
|
44
|
-
const { request, currentLocaleCode } = useApp();
|
|
45
|
-
const access = useOperationsAccess();
|
|
46
|
-
|
|
47
|
-
const {
|
|
48
|
-
data: dashboard,
|
|
49
|
-
isLoading,
|
|
50
|
-
refetch,
|
|
51
|
-
} = useQuery<OperationsDashboard>({
|
|
52
|
-
queryKey: ['operations-dashboard', currentLocaleCode],
|
|
53
|
-
queryFn: () =>
|
|
54
|
-
fetchOperations<OperationsDashboard>(request, '/operations/dashboard'),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const { data: team } = useQuery<OperationsTeamOverview>({
|
|
58
|
-
queryKey: ['operations-team-overview', currentLocaleCode],
|
|
59
|
-
enabled: access.isSupervisor,
|
|
60
|
-
queryFn: () =>
|
|
61
|
-
fetchOperations<OperationsTeamOverview>(request, '/operations/team'),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const cards = dashboard
|
|
65
|
-
? [
|
|
66
|
-
{
|
|
67
|
-
key: 'projects',
|
|
68
|
-
title: t('cards.projects'),
|
|
69
|
-
value: dashboard.cards.projectsTotal,
|
|
70
|
-
description: t('cards.projectsDescription', {
|
|
71
|
-
active: dashboard.cards.activeProjects,
|
|
72
|
-
}),
|
|
73
|
-
icon: FolderKanban,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
key: 'timesheets',
|
|
77
|
-
title: t('cards.timesheets'),
|
|
78
|
-
value: dashboard.cards.visibleTimesheets,
|
|
79
|
-
description: t('cards.timesheetsDescription', {
|
|
80
|
-
pending: dashboard.cards.pendingTimesheets,
|
|
81
|
-
}),
|
|
82
|
-
icon: ClipboardList,
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
key: 'timeOff',
|
|
86
|
-
title: t('cards.timeOff'),
|
|
87
|
-
value: dashboard.cards.timeOffRequests,
|
|
88
|
-
description: t('cards.timeOffDescription'),
|
|
89
|
-
icon: PlaneTakeoff,
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
key: 'approvals',
|
|
93
|
-
title: t('cards.approvals'),
|
|
94
|
-
value: dashboard.cards.pendingApprovals,
|
|
95
|
-
description: t('cards.approvalsDescription'),
|
|
96
|
-
icon: Hourglass,
|
|
97
|
-
},
|
|
98
|
-
]
|
|
99
|
-
: [];
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<Page>
|
|
103
|
-
<OperationsHeader
|
|
104
|
-
title={t('title')}
|
|
105
|
-
description={t('description')}
|
|
106
|
-
current={t('breadcrumb')}
|
|
107
|
-
actions={
|
|
108
|
-
<div className="flex flex-wrap gap-2">
|
|
109
|
-
<Button variant="outline" size="sm" onClick={() => void refetch()}>
|
|
110
|
-
<RefreshCcw className="size-4" />
|
|
111
|
-
{commonT('actions.refresh')}
|
|
112
|
-
</Button>
|
|
113
|
-
{access.isDirector ? (
|
|
114
|
-
<Button variant="outline" size="sm" asChild>
|
|
115
|
-
<Link href="/operations/departments">
|
|
116
|
-
<Building2 className="size-4" />
|
|
117
|
-
{commonT('actions.manageDepartments')}
|
|
118
|
-
</Link>
|
|
119
|
-
</Button>
|
|
120
|
-
) : null}
|
|
121
|
-
<Button asChild size="sm">
|
|
122
|
-
<Link href="/operations/timesheets">
|
|
123
|
-
{t('actions.openTimesheets')}
|
|
124
|
-
</Link>
|
|
125
|
-
</Button>
|
|
126
|
-
</div>
|
|
127
|
-
}
|
|
128
|
-
/>
|
|
129
|
-
|
|
130
|
-
{isLoading || access.isLoading ? (
|
|
131
|
-
<div className="space-y-4">
|
|
132
|
-
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
133
|
-
{Array.from({ length: 4 }).map((_, index) => (
|
|
134
|
-
<Skeleton key={index} className="h-32 w-full" />
|
|
135
|
-
))}
|
|
136
|
-
</div>
|
|
137
|
-
<div className="grid gap-4 xl:grid-cols-3">
|
|
138
|
-
<Skeleton className="h-64 w-full xl:col-span-2" />
|
|
139
|
-
<Skeleton className="h-64 w-full" />
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
) : dashboard ? (
|
|
143
|
-
<>
|
|
144
|
-
<KpiCardsGrid items={cards} />
|
|
145
|
-
|
|
146
|
-
<div className="grid gap-4 xl:grid-cols-3">
|
|
147
|
-
<SectionCard
|
|
148
|
-
title={t('scope.title')}
|
|
149
|
-
description={t('scope.description')}
|
|
150
|
-
className="xl:col-span-1"
|
|
151
|
-
>
|
|
152
|
-
<dl className="space-y-3 text-sm">
|
|
153
|
-
<div className="flex items-center justify-between gap-4">
|
|
154
|
-
<dt className="text-muted-foreground">
|
|
155
|
-
{t('scope.roleScope')}
|
|
156
|
-
</dt>
|
|
157
|
-
<dd className="font-medium">
|
|
158
|
-
{formatEnumLabel(dashboard.actor.roleScope)}
|
|
159
|
-
</dd>
|
|
160
|
-
</div>
|
|
161
|
-
<div className="flex items-center justify-between gap-4">
|
|
162
|
-
<dt className="text-muted-foreground">
|
|
163
|
-
{t('scope.collaborator')}
|
|
164
|
-
</dt>
|
|
165
|
-
<dd className="font-medium">
|
|
166
|
-
{dashboard.actor.collaboratorName ??
|
|
167
|
-
commonT('labels.notAvailable')}
|
|
168
|
-
</dd>
|
|
169
|
-
</div>
|
|
170
|
-
<div className="flex items-center justify-between gap-4">
|
|
171
|
-
<dt className="text-muted-foreground">
|
|
172
|
-
{t('scope.teamSize')}
|
|
173
|
-
</dt>
|
|
174
|
-
<dd className="font-medium">{dashboard.actor.teamSize}</dd>
|
|
175
|
-
</div>
|
|
176
|
-
</dl>
|
|
177
|
-
</SectionCard>
|
|
178
|
-
|
|
179
|
-
<SectionCard
|
|
180
|
-
title={t('recentTimesheets.title')}
|
|
181
|
-
description={t('recentTimesheets.description')}
|
|
182
|
-
className="xl:col-span-2"
|
|
183
|
-
>
|
|
184
|
-
{dashboard.recentTimesheets.length === 0 ? (
|
|
185
|
-
<EmptyState
|
|
186
|
-
icon={<ClipboardList className="size-12" />}
|
|
187
|
-
title={commonT('states.emptyTitle')}
|
|
188
|
-
description={t('recentTimesheets.empty')}
|
|
189
|
-
actionLabel={t('actions.openTimesheets')}
|
|
190
|
-
onAction={() => {
|
|
191
|
-
window.location.href = '/operations/timesheets';
|
|
192
|
-
}}
|
|
193
|
-
/>
|
|
194
|
-
) : (
|
|
195
|
-
<div className="overflow-x-auto rounded-md border">
|
|
196
|
-
<Table>
|
|
197
|
-
<TableHeader>
|
|
198
|
-
<TableRow>
|
|
199
|
-
<TableHead>{commonT('labels.collaborator')}</TableHead>
|
|
200
|
-
<TableHead>{commonT('labels.week')}</TableHead>
|
|
201
|
-
<TableHead>{commonT('labels.totalHours')}</TableHead>
|
|
202
|
-
<TableHead>{commonT('labels.status')}</TableHead>
|
|
203
|
-
</TableRow>
|
|
204
|
-
</TableHeader>
|
|
205
|
-
<TableBody>
|
|
206
|
-
{dashboard.recentTimesheets.map((item) => (
|
|
207
|
-
<TableRow key={item.id}>
|
|
208
|
-
<TableCell className="font-medium">
|
|
209
|
-
{item.collaboratorName}
|
|
210
|
-
</TableCell>
|
|
211
|
-
<TableCell>
|
|
212
|
-
{formatDateRange(
|
|
213
|
-
item.weekStartDate,
|
|
214
|
-
item.weekEndDate
|
|
215
|
-
)}
|
|
216
|
-
</TableCell>
|
|
217
|
-
<TableCell>{formatHours(item.totalHours)}</TableCell>
|
|
218
|
-
<TableCell>
|
|
219
|
-
<StatusBadge
|
|
220
|
-
label={formatEnumLabel(item.status)}
|
|
221
|
-
className={getStatusBadgeClass(item.status)}
|
|
222
|
-
/>
|
|
223
|
-
</TableCell>
|
|
224
|
-
</TableRow>
|
|
225
|
-
))}
|
|
226
|
-
</TableBody>
|
|
227
|
-
</Table>
|
|
228
|
-
</div>
|
|
229
|
-
)}
|
|
230
|
-
</SectionCard>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<div className="grid gap-4 xl:grid-cols-2">
|
|
234
|
-
<SectionCard
|
|
235
|
-
title={t('nextSteps.title')}
|
|
236
|
-
description={t('nextSteps.description')}
|
|
237
|
-
>
|
|
238
|
-
<div className="space-y-3 text-sm">
|
|
239
|
-
<div className="flex items-start gap-3">
|
|
240
|
-
<CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
|
|
241
|
-
<p>{t('nextSteps.submitTimesheet')}</p>
|
|
242
|
-
</div>
|
|
243
|
-
<div className="flex items-start gap-3">
|
|
244
|
-
<CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
|
|
245
|
-
<p>{t('nextSteps.requestTimeOff')}</p>
|
|
246
|
-
</div>
|
|
247
|
-
<div className="flex items-start gap-3">
|
|
248
|
-
<CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
|
|
249
|
-
<p>{t('nextSteps.adjustSchedule')}</p>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
</SectionCard>
|
|
253
|
-
|
|
254
|
-
<SectionCard
|
|
255
|
-
title={t('team.title')}
|
|
256
|
-
description={t('team.description')}
|
|
257
|
-
>
|
|
258
|
-
{access.isSupervisor && team ? (
|
|
259
|
-
<>
|
|
260
|
-
<div className="mb-4 grid gap-3 md:grid-cols-3">
|
|
261
|
-
<div className="rounded-lg border p-4">
|
|
262
|
-
<div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
|
263
|
-
{t('team.members')}
|
|
264
|
-
</div>
|
|
265
|
-
<div className="mt-2 text-2xl font-semibold">
|
|
266
|
-
{team.teamMembers.length}
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
<div className="rounded-lg border p-4">
|
|
270
|
-
<div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
|
271
|
-
{t('team.projects')}
|
|
272
|
-
</div>
|
|
273
|
-
<div className="mt-2 text-2xl font-semibold">
|
|
274
|
-
{team.projectCount}
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
<div className="rounded-lg border p-4">
|
|
278
|
-
<div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
|
279
|
-
{t('team.pendingApprovals')}
|
|
280
|
-
</div>
|
|
281
|
-
<div className="mt-2 text-2xl font-semibold">
|
|
282
|
-
{team.pendingApprovals}
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
{team.teamMembers.length > 0 ? (
|
|
288
|
-
<div className="space-y-3">
|
|
289
|
-
{team.teamMembers.slice(0, 4).map((member) => (
|
|
290
|
-
<div
|
|
291
|
-
key={member.id}
|
|
292
|
-
className="flex items-center justify-between rounded-lg border px-4 py-3 text-sm"
|
|
293
|
-
>
|
|
294
|
-
<div>
|
|
295
|
-
<div className="font-medium">
|
|
296
|
-
{member.displayName}
|
|
297
|
-
</div>
|
|
298
|
-
<div className="text-muted-foreground">
|
|
299
|
-
{[member.department, member.title]
|
|
300
|
-
.filter(Boolean)
|
|
301
|
-
.join(' • ') || commonT('labels.notAvailable')}
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
<div className="text-right">
|
|
305
|
-
<div className="font-medium">
|
|
306
|
-
{member.activeAssignments ?? 0}{' '}
|
|
307
|
-
{t('team.assignments')}
|
|
308
|
-
</div>
|
|
309
|
-
<div className="text-muted-foreground">
|
|
310
|
-
{member.pendingApprovals ?? 0}{' '}
|
|
311
|
-
{t('team.awaitingReview')}
|
|
312
|
-
</div>
|
|
313
|
-
</div>
|
|
314
|
-
</div>
|
|
315
|
-
))}
|
|
316
|
-
</div>
|
|
317
|
-
) : (
|
|
318
|
-
<p className="text-sm text-muted-foreground">
|
|
319
|
-
{t('team.empty')}
|
|
320
|
-
</p>
|
|
321
|
-
)}
|
|
322
|
-
</>
|
|
323
|
-
) : (
|
|
324
|
-
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
|
|
325
|
-
<Users className="mt-0.5 size-4" />
|
|
326
|
-
<p>{t('team.collaboratorMessage')}</p>
|
|
327
|
-
</div>
|
|
328
|
-
)}
|
|
329
|
-
</SectionCard>
|
|
330
|
-
</div>
|
|
331
|
-
</>
|
|
332
|
-
) : (
|
|
333
|
-
<EmptyState
|
|
334
|
-
icon={<FolderKanban className="size-12" />}
|
|
335
|
-
title={commonT('states.emptyTitle')}
|
|
336
|
-
description={commonT('states.emptyDescription')}
|
|
337
|
-
actionLabel={commonT('actions.refresh')}
|
|
338
|
-
onAction={() => void refetch()}
|
|
339
|
-
/>
|
|
340
|
-
)}
|
|
341
|
-
</Page>
|
|
342
|
-
);
|
|
3
|
+
export default function OperationsPage() {
|
|
4
|
+
redirect('/operations/projects');
|
|
343
5
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/operations",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.302",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"@nestjs/core": "^11",
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
|
-
"@hed-hog/api": "0.0.
|
|
13
|
-
"@hed-hog/api-types": "0.0.1",
|
|
12
|
+
"@hed-hog/api-pagination": "0.0.7",
|
|
14
13
|
"@hed-hog/api-prisma": "0.0.6",
|
|
15
14
|
"@hed-hog/api-locale": "0.0.14",
|
|
16
|
-
"@hed-hog/api
|
|
17
|
-
"@hed-hog/core": "0.0.
|
|
18
|
-
"@hed-hog/contact": "0.0.
|
|
15
|
+
"@hed-hog/api": "0.0.6",
|
|
16
|
+
"@hed-hog/core": "0.0.302",
|
|
17
|
+
"@hed-hog/contact": "0.0.302",
|
|
18
|
+
"@hed-hog/api-types": "0.0.1"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
".": {
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ForbiddenException,
|
|
11
11
|
Inject,
|
|
12
12
|
Injectable,
|
|
13
|
+
InternalServerErrorException,
|
|
14
|
+
Logger,
|
|
13
15
|
NotFoundException,
|
|
14
16
|
forwardRef,
|
|
15
17
|
} from '@nestjs/common';
|
|
@@ -621,6 +623,8 @@ type PublishAccountsPayableReferencePayload = {
|
|
|
621
623
|
|
|
622
624
|
@Injectable()
|
|
623
625
|
export class OperationsService {
|
|
626
|
+
private readonly logger = new Logger(OperationsService.name);
|
|
627
|
+
|
|
624
628
|
constructor(
|
|
625
629
|
private readonly prisma: PrismaService,
|
|
626
630
|
private readonly aiService: AiService,
|
|
@@ -3107,10 +3111,6 @@ export class OperationsService {
|
|
|
3107
3111
|
select: {
|
|
3108
3112
|
id: true,
|
|
3109
3113
|
name: true,
|
|
3110
|
-
trade_name: true,
|
|
3111
|
-
email: true,
|
|
3112
|
-
phone: true,
|
|
3113
|
-
document: true,
|
|
3114
3114
|
},
|
|
3115
3115
|
},
|
|
3116
3116
|
},
|
|
@@ -3118,7 +3118,54 @@ export class OperationsService {
|
|
|
3118
3118
|
|
|
3119
3119
|
if (proposal) {
|
|
3120
3120
|
personId = proposal.person_id ?? proposal.person?.id ?? null;
|
|
3121
|
-
|
|
3121
|
+
|
|
3122
|
+
const [companyRow, contactRows, documentRows] =
|
|
3123
|
+
personId != null
|
|
3124
|
+
? await Promise.all([
|
|
3125
|
+
(this.prisma as any).person_company.findUnique({
|
|
3126
|
+
where: { id: personId },
|
|
3127
|
+
select: { trade_name: true },
|
|
3128
|
+
}),
|
|
3129
|
+
(this.prisma as any).contact.findMany({
|
|
3130
|
+
where: { person_id: personId },
|
|
3131
|
+
include: {
|
|
3132
|
+
contact_type: {
|
|
3133
|
+
select: { code: true },
|
|
3134
|
+
},
|
|
3135
|
+
},
|
|
3136
|
+
orderBy: [{ is_primary: 'desc' }, { id: 'asc' }],
|
|
3137
|
+
}),
|
|
3138
|
+
(this.prisma as any).document.findMany({
|
|
3139
|
+
where: { person_id: personId },
|
|
3140
|
+
select: { value: true },
|
|
3141
|
+
orderBy: [{ id: 'asc' }],
|
|
3142
|
+
}),
|
|
3143
|
+
])
|
|
3144
|
+
: [null, [], []];
|
|
3145
|
+
|
|
3146
|
+
const resolveContactValue = (codes: string[]) => {
|
|
3147
|
+
const normalizedCodes = new Set(codes.map((code) => code.toUpperCase()));
|
|
3148
|
+
const items = Array.isArray(contactRows)
|
|
3149
|
+
? contactRows.filter((contact: any) =>
|
|
3150
|
+
normalizedCodes.has(
|
|
3151
|
+
String(contact?.contact_type?.code || '').toUpperCase(),
|
|
3152
|
+
),
|
|
3153
|
+
)
|
|
3154
|
+
: [];
|
|
3155
|
+
const primary = items.find((contact: any) => contact?.is_primary);
|
|
3156
|
+
const fallback = items[0];
|
|
3157
|
+
return primary?.value ?? fallback?.value ?? null;
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3160
|
+
personRecord = proposal.person
|
|
3161
|
+
? {
|
|
3162
|
+
...proposal.person,
|
|
3163
|
+
trade_name: companyRow?.trade_name ?? null,
|
|
3164
|
+
email: resolveContactValue(['EMAIL']),
|
|
3165
|
+
phone: resolveContactValue(['PHONE', 'MOBILE', 'WHATSAPP']),
|
|
3166
|
+
document: documentRows[0]?.value ?? null,
|
|
3167
|
+
}
|
|
3168
|
+
: null;
|
|
3122
3169
|
}
|
|
3123
3170
|
}
|
|
3124
3171
|
}
|
|
@@ -6194,8 +6241,24 @@ export class OperationsService {
|
|
|
6194
6241
|
})
|
|
6195
6242
|
);
|
|
6196
6243
|
} catch (error) {
|
|
6197
|
-
|
|
6198
|
-
|
|
6244
|
+
const errorMessage =
|
|
6245
|
+
error instanceof Error ? error.message : String(error);
|
|
6246
|
+
const errorStack =
|
|
6247
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
6248
|
+
const missingPlaywrightRuntime =
|
|
6249
|
+
/Cannot find package ['"]playwright['"]|Cannot find module ['"]playwright['"]|Executable doesn't exist|browserType\.launch|Failed to launch|Please run the following command to download new browsers/i.test(
|
|
6250
|
+
errorMessage
|
|
6251
|
+
);
|
|
6252
|
+
|
|
6253
|
+
this.logger.error(
|
|
6254
|
+
`Failed to generate contract PDF for contract ${contract?.id ?? 'unknown'}. ${errorMessage}`,
|
|
6255
|
+
errorStack
|
|
6256
|
+
);
|
|
6257
|
+
|
|
6258
|
+
throw new InternalServerErrorException(
|
|
6259
|
+
missingPlaywrightRuntime
|
|
6260
|
+
? 'PDF generation is unavailable because Playwright/Chromium is not installed on the server. Run `pnpm --filter api run playwright:install` in the API environment.'
|
|
6261
|
+
: 'Failed to generate the PDF document. Check server logs for details.'
|
|
6199
6262
|
);
|
|
6200
6263
|
} finally {
|
|
6201
6264
|
await browser?.close?.();
|