@hed-hog/operations 0.0.301 → 0.0.303
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/contracts/page.tsx.ejs +99 -102
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +87 -93
- package/hedhog/frontend/app/departments/page.tsx.ejs +95 -74
- package/hedhog/frontend/app/page.tsx.ejs +3 -341
- package/hedhog/frontend/app/projects/page.tsx.ejs +133 -127
- package/package.json +6 -6
- package/src/operations.service.ts +70 -7
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { EmptyState, Page, SearchBar } from '@/components/entity-list';
|
|
4
4
|
import { Button } from '@/components/ui/button';
|
|
5
|
-
import { Card, CardContent } from '@/components/ui/card';
|
|
6
5
|
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
7
6
|
import {
|
|
8
7
|
Sheet,
|
|
@@ -65,7 +64,10 @@ export default function OperationsProjectsPage() {
|
|
|
65
64
|
const editProjectId = parseEditProjectId(searchParams.get('edit'));
|
|
66
65
|
const isSheetOpen = createParam === '1' || editProjectId !== null;
|
|
67
66
|
|
|
68
|
-
const updateSheetQuery = (next: {
|
|
67
|
+
const updateSheetQuery = (next: {
|
|
68
|
+
create?: boolean;
|
|
69
|
+
editId?: number | null;
|
|
70
|
+
}) => {
|
|
69
71
|
const params = new URLSearchParams(searchParams.toString());
|
|
70
72
|
|
|
71
73
|
params.delete('create');
|
|
@@ -117,7 +119,9 @@ export default function OperationsProjectsPage() {
|
|
|
117
119
|
]
|
|
118
120
|
.filter(Boolean)
|
|
119
121
|
.some((value) =>
|
|
120
|
-
String(value)
|
|
122
|
+
String(value)
|
|
123
|
+
.toLowerCase()
|
|
124
|
+
.includes(search.trim().toLowerCase())
|
|
121
125
|
);
|
|
122
126
|
|
|
123
127
|
const matchesStatus =
|
|
@@ -220,139 +224,141 @@ export default function OperationsProjectsPage() {
|
|
|
220
224
|
/>
|
|
221
225
|
|
|
222
226
|
{filteredRows.length > 0 ? (
|
|
223
|
-
<
|
|
224
|
-
<
|
|
225
|
-
<
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
{
|
|
255
|
-
<
|
|
256
|
-
<
|
|
257
|
-
<div className="
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<TableCell>
|
|
267
|
-
<div className="truncate">
|
|
268
|
-
{project.clientName || commonT('labels.notAvailable')}
|
|
227
|
+
<div className="overflow-x-auto rounded-md border">
|
|
228
|
+
<Table className="table-fixed">
|
|
229
|
+
<TableHeader>
|
|
230
|
+
<TableRow>
|
|
231
|
+
<TableHead className="w-[30%]">
|
|
232
|
+
{commonT('labels.project')}
|
|
233
|
+
</TableHead>
|
|
234
|
+
<TableHead>{commonT('labels.client')}</TableHead>
|
|
235
|
+
<TableHead>{commonT('labels.status')}</TableHead>
|
|
236
|
+
<TableHead className="hidden lg:table-cell">
|
|
237
|
+
{commonT('labels.manager')}
|
|
238
|
+
</TableHead>
|
|
239
|
+
<TableHead className="hidden md:table-cell">
|
|
240
|
+
{commonT('labels.teamSize')}
|
|
241
|
+
</TableHead>
|
|
242
|
+
<TableHead className="hidden xl:table-cell">
|
|
243
|
+
{commonT('labels.startDate')}
|
|
244
|
+
</TableHead>
|
|
245
|
+
<TableHead className="hidden xl:table-cell">
|
|
246
|
+
{commonT('labels.endDate')}
|
|
247
|
+
</TableHead>
|
|
248
|
+
<TableHead className="hidden 2xl:table-cell">
|
|
249
|
+
{commonT('labels.contractStatus')}
|
|
250
|
+
</TableHead>
|
|
251
|
+
<TableHead className="w-30 text-right sm:w-42.5">
|
|
252
|
+
{commonT('labels.actions')}
|
|
253
|
+
</TableHead>
|
|
254
|
+
</TableRow>
|
|
255
|
+
</TableHeader>
|
|
256
|
+
<TableBody>
|
|
257
|
+
{filteredRows.map((project) => (
|
|
258
|
+
<TableRow key={project.id} className="hover:bg-muted/30">
|
|
259
|
+
<TableCell>
|
|
260
|
+
<div className="min-w-0">
|
|
261
|
+
<div className="truncate font-medium">{project.name}</div>
|
|
262
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
263
|
+
{[
|
|
264
|
+
project.code,
|
|
265
|
+
project.myRoleLabel,
|
|
266
|
+
project.contractName,
|
|
267
|
+
]
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
.join(' • ') || commonT('labels.notAvailable')}
|
|
269
270
|
</div>
|
|
270
|
-
</
|
|
271
|
-
|
|
271
|
+
</div>
|
|
272
|
+
</TableCell>
|
|
273
|
+
<TableCell>
|
|
274
|
+
<div className="truncate">
|
|
275
|
+
{project.clientName || commonT('labels.notAvailable')}
|
|
276
|
+
</div>
|
|
277
|
+
</TableCell>
|
|
278
|
+
<TableCell>
|
|
279
|
+
<StatusBadge
|
|
280
|
+
label={formatEnumLabel(project.status)}
|
|
281
|
+
className={getStatusBadgeClass(project.status)}
|
|
282
|
+
/>
|
|
283
|
+
</TableCell>
|
|
284
|
+
<TableCell className="hidden lg:table-cell">
|
|
285
|
+
<div className="truncate">
|
|
286
|
+
{project.managerName || commonT('labels.notAssigned')}
|
|
287
|
+
</div>
|
|
288
|
+
</TableCell>
|
|
289
|
+
<TableCell className="hidden md:table-cell">
|
|
290
|
+
{project.teamSize ?? 0}
|
|
291
|
+
</TableCell>
|
|
292
|
+
<TableCell className="hidden xl:table-cell">
|
|
293
|
+
{formatDate(project.startDate)}
|
|
294
|
+
</TableCell>
|
|
295
|
+
<TableCell className="hidden xl:table-cell">
|
|
296
|
+
{formatDate(project.endDate)}
|
|
297
|
+
</TableCell>
|
|
298
|
+
<TableCell className="hidden 2xl:table-cell">
|
|
299
|
+
{project.contractStatus ? (
|
|
272
300
|
<StatusBadge
|
|
273
|
-
label={formatEnumLabel(project.
|
|
274
|
-
className={getStatusBadgeClass(project.
|
|
301
|
+
label={formatEnumLabel(project.contractStatus)}
|
|
302
|
+
className={getStatusBadgeClass(project.contractStatus)}
|
|
275
303
|
/>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
<TableCell className="hidden xl:table-cell">
|
|
289
|
-
{formatDate(project.endDate)}
|
|
290
|
-
</TableCell>
|
|
291
|
-
<TableCell className="hidden 2xl:table-cell">
|
|
292
|
-
{project.contractStatus ? (
|
|
293
|
-
<StatusBadge
|
|
294
|
-
label={formatEnumLabel(project.contractStatus)}
|
|
295
|
-
className={getStatusBadgeClass(project.contractStatus)}
|
|
296
|
-
/>
|
|
297
|
-
) : (
|
|
298
|
-
commonT('labels.notAssigned')
|
|
299
|
-
)}
|
|
300
|
-
</TableCell>
|
|
301
|
-
<TableCell>
|
|
302
|
-
<div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
|
|
303
|
-
<Button variant="outline" size="icon" asChild>
|
|
304
|
-
<Link href={`/operations/projects/${project.id}`}>
|
|
305
|
-
<Eye className="size-4" />
|
|
306
|
-
</Link>
|
|
307
|
-
</Button>
|
|
308
|
-
{access.isDirector ? (
|
|
309
|
-
<Button
|
|
310
|
-
variant="outline"
|
|
311
|
-
size="icon"
|
|
312
|
-
className="cursor-pointer"
|
|
313
|
-
onClick={() => openEditSheet(project.id)}
|
|
314
|
-
>
|
|
315
|
-
<Pencil className="size-4" />
|
|
316
|
-
</Button>
|
|
317
|
-
) : null}
|
|
304
|
+
) : (
|
|
305
|
+
commonT('labels.notAssigned')
|
|
306
|
+
)}
|
|
307
|
+
</TableCell>
|
|
308
|
+
<TableCell>
|
|
309
|
+
<div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
|
|
310
|
+
<Button variant="outline" size="icon" asChild>
|
|
311
|
+
<Link href={`/operations/projects/${project.id}`}>
|
|
312
|
+
<Eye className="size-4" />
|
|
313
|
+
</Link>
|
|
314
|
+
</Button>
|
|
315
|
+
{access.isDirector ? (
|
|
318
316
|
<Button
|
|
319
317
|
variant="outline"
|
|
320
318
|
size="icon"
|
|
321
|
-
|
|
322
|
-
|
|
319
|
+
className="cursor-pointer"
|
|
320
|
+
onClick={() => openEditSheet(project.id)}
|
|
323
321
|
>
|
|
324
|
-
|
|
325
|
-
<Link
|
|
326
|
-
href={`/operations/contracts?edit=${project.contractId}`}
|
|
327
|
-
>
|
|
328
|
-
<FileText className="size-4" />
|
|
329
|
-
</Link>
|
|
330
|
-
) : (
|
|
331
|
-
<span>
|
|
332
|
-
<FileText className="size-4" />
|
|
333
|
-
</span>
|
|
334
|
-
)}
|
|
322
|
+
<Pencil className="size-4" />
|
|
335
323
|
</Button>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
324
|
+
) : null}
|
|
325
|
+
<Button
|
|
326
|
+
variant="outline"
|
|
327
|
+
size="icon"
|
|
328
|
+
asChild={Boolean(project.contractId)}
|
|
329
|
+
disabled={!project.contractId}
|
|
330
|
+
>
|
|
331
|
+
{project.contractId ? (
|
|
332
|
+
<Link
|
|
333
|
+
href={`/operations/contracts?edit=${project.contractId}`}
|
|
342
334
|
>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
335
|
+
<FileText className="size-4" />
|
|
336
|
+
</Link>
|
|
337
|
+
) : (
|
|
338
|
+
<span>
|
|
339
|
+
<FileText className="size-4" />
|
|
340
|
+
</span>
|
|
341
|
+
)}
|
|
342
|
+
</Button>
|
|
343
|
+
{access.isDirector ? (
|
|
344
|
+
<Button
|
|
345
|
+
variant="outline"
|
|
346
|
+
size="sm"
|
|
347
|
+
className="cursor-pointer"
|
|
348
|
+
onClick={() => void toggleArchived(project)}
|
|
349
|
+
>
|
|
350
|
+
{project.status === 'archived'
|
|
351
|
+
? commonT('actions.activate')
|
|
352
|
+
: t('actions.archive')}
|
|
353
|
+
</Button>
|
|
354
|
+
) : null}
|
|
355
|
+
</div>
|
|
356
|
+
</TableCell>
|
|
357
|
+
</TableRow>
|
|
358
|
+
))}
|
|
359
|
+
</TableBody>
|
|
360
|
+
</Table>
|
|
361
|
+
</div>
|
|
356
362
|
) : (
|
|
357
363
|
<EmptyState
|
|
358
364
|
icon={<FolderKanban className="size-12" />}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/operations",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.303",
|
|
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",
|
|
14
|
+
"@hed-hog/api-types": "0.0.1",
|
|
15
15
|
"@hed-hog/api-locale": "0.0.14",
|
|
16
|
-
"@hed-hog/api
|
|
17
|
-
"@hed-hog/core": "0.0.
|
|
18
|
-
"@hed-hog/contact": "0.0.
|
|
16
|
+
"@hed-hog/api": "0.0.6",
|
|
17
|
+
"@hed-hog/core": "0.0.303",
|
|
18
|
+
"@hed-hog/contact": "0.0.303"
|
|
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?.();
|