@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.
@@ -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: { create?: boolean; editId?: number | null }) => {
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).toLowerCase().includes(search.trim().toLowerCase())
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
- <Card className="overflow-hidden border-border/60 py-0 shadow-sm">
224
- <CardContent className="p-0">
225
- <Table className="table-fixed">
226
- <TableHeader>
227
- <TableRow>
228
- <TableHead className="w-[30%]">
229
- {commonT('labels.project')}
230
- </TableHead>
231
- <TableHead>{commonT('labels.client')}</TableHead>
232
- <TableHead>{commonT('labels.status')}</TableHead>
233
- <TableHead className="hidden lg:table-cell">
234
- {commonT('labels.manager')}
235
- </TableHead>
236
- <TableHead className="hidden md:table-cell">
237
- {commonT('labels.teamSize')}
238
- </TableHead>
239
- <TableHead className="hidden xl:table-cell">
240
- {commonT('labels.startDate')}
241
- </TableHead>
242
- <TableHead className="hidden xl:table-cell">
243
- {commonT('labels.endDate')}
244
- </TableHead>
245
- <TableHead className="hidden 2xl:table-cell">
246
- {commonT('labels.contractStatus')}
247
- </TableHead>
248
- <TableHead className="w-30 text-right sm:w-42.5">
249
- {commonT('labels.actions')}
250
- </TableHead>
251
- </TableRow>
252
- </TableHeader>
253
- <TableBody>
254
- {filteredRows.map((project) => (
255
- <TableRow key={project.id} className="hover:bg-muted/30">
256
- <TableCell>
257
- <div className="min-w-0">
258
- <div className="truncate font-medium">{project.name}</div>
259
- <div className="truncate text-xs text-muted-foreground">
260
- {[project.code, project.myRoleLabel, project.contractName]
261
- .filter(Boolean)
262
- .join(' • ') || commonT('labels.notAvailable')}
263
- </div>
264
- </div>
265
- </TableCell>
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
- </TableCell>
271
- <TableCell>
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.status)}
274
- className={getStatusBadgeClass(project.status)}
301
+ label={formatEnumLabel(project.contractStatus)}
302
+ className={getStatusBadgeClass(project.contractStatus)}
275
303
  />
276
- </TableCell>
277
- <TableCell className="hidden lg:table-cell">
278
- <div className="truncate">
279
- {project.managerName || commonT('labels.notAssigned')}
280
- </div>
281
- </TableCell>
282
- <TableCell className="hidden md:table-cell">
283
- {project.teamSize ?? 0}
284
- </TableCell>
285
- <TableCell className="hidden xl:table-cell">
286
- {formatDate(project.startDate)}
287
- </TableCell>
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
- asChild={Boolean(project.contractId)}
322
- disabled={!project.contractId}
319
+ className="cursor-pointer"
320
+ onClick={() => openEditSheet(project.id)}
323
321
  >
324
- {project.contractId ? (
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
- {access.isDirector ? (
337
- <Button
338
- variant="outline"
339
- size="sm"
340
- className="cursor-pointer"
341
- onClick={() => void toggleArchived(project)}
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
- {project.status === 'archived'
344
- ? commonT('actions.activate')
345
- : t('actions.archive')}
346
- </Button>
347
- ) : null}
348
- </div>
349
- </TableCell>
350
- </TableRow>
351
- ))}
352
- </TableBody>
353
- </Table>
354
- </CardContent>
355
- </Card>
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.301",
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.6",
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-pagination": "0.0.7",
17
- "@hed-hog/core": "0.0.301",
18
- "@hed-hog/contact": "0.0.301"
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
- personRecord = proposal.person ?? null;
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
- throw new BadRequestException(
6198
- 'PDF generation requires Playwright to be installed on the server.'
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?.();