@hed-hog/operations 0.0.304 → 0.0.305

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 (52) hide show
  1. package/dist/controllers/operations-projects.controller.d.ts +15 -0
  2. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-tasks.controller.d.ts +41 -10
  4. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  5. package/dist/controllers/operations-tasks.controller.js +11 -0
  6. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  7. package/dist/dto/create-task.dto.d.ts +7 -1
  8. package/dist/dto/create-task.dto.d.ts.map +1 -1
  9. package/dist/dto/create-task.dto.js +38 -5
  10. package/dist/dto/create-task.dto.js.map +1 -1
  11. package/dist/dto/list-tasks.dto.d.ts +1 -1
  12. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  13. package/dist/dto/list-tasks.dto.js +2 -2
  14. package/dist/dto/list-tasks.dto.js.map +1 -1
  15. package/dist/dto/update-task.dto.d.ts +7 -1
  16. package/dist/dto/update-task.dto.d.ts.map +1 -1
  17. package/dist/dto/update-task.dto.js +38 -5
  18. package/dist/dto/update-task.dto.js.map +1 -1
  19. package/dist/operations.service.d.ts +68 -12
  20. package/dist/operations.service.d.ts.map +1 -1
  21. package/dist/operations.service.js +380 -101
  22. package/dist/operations.service.js.map +1 -1
  23. package/hedhog/data/route.yaml +13 -0
  24. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +44 -44
  25. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +168 -213
  26. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -256
  27. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +7 -7
  28. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +306 -306
  29. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -247
  30. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -3520
  31. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +1504 -52
  32. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +528 -403
  33. package/hedhog/frontend/app/_components/section-card.tsx.ejs +25 -18
  34. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +609 -0
  35. package/hedhog/frontend/app/_lib/types.ts.ejs +5 -0
  36. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +7 -7
  37. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +48 -1
  38. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +502 -502
  39. package/hedhog/frontend/app/collaborators/page.tsx.ejs +10 -7
  40. package/hedhog/frontend/app/contracts/page.tsx.ejs +938 -938
  41. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +1 -1
  42. package/hedhog/frontend/app/projects/page.tsx.ejs +360 -133
  43. package/hedhog/frontend/messages/en.json +27 -4
  44. package/hedhog/frontend/messages/pt.json +27 -4
  45. package/hedhog/table/operations_project.yaml +9 -0
  46. package/hedhog/table/operations_task.yaml +43 -4
  47. package/package.json +5 -5
  48. package/src/controllers/operations-tasks.controller.ts +11 -0
  49. package/src/dto/create-task.dto.ts +47 -7
  50. package/src/dto/list-tasks.dto.ts +3 -3
  51. package/src/dto/update-task.dto.ts +47 -7
  52. package/src/operations.service.ts +556 -88
@@ -107,7 +107,7 @@ const FINANCIAL_TERM_TYPE_VALUES = ['value', 'payment', 'revenue', 'fine', 'othe
107
107
  const RECURRENCE_VALUES = ['one_time', 'monthly', 'quarterly', 'yearly', 'other'] as const;
108
108
  const REVISION_TYPE_VALUES = ['amendment', 'renewal', 'revision', 'addendum', 'other'] as const;
109
109
  const REVISION_STATUS_VALUES = ['draft', 'active', 'completed', 'cancelled'] as const;
110
- const TASK_STATUS_VALUES = ['active', 'completed', 'archived'] as const;
110
+ const TASK_STATUS_VALUES = ['todo', 'doing', 'review', 'done'] as const;
111
111
 
112
112
  type ApprovalAction = 'approve' | 'reject';
113
113
  type ApprovalTargetType =
@@ -548,6 +548,7 @@ type ProjectPayload = {
548
548
  contractId?: number | null;
549
549
  contractTemplateId?: number | null;
550
550
  managerCollaboratorId?: number | null;
551
+ clientPersonId?: number | null;
551
552
  code: string;
552
553
  name: string;
553
554
  clientName?: string | null;
@@ -606,9 +607,15 @@ type TimesheetEntryPayload = {
606
607
  type TaskPayload = {
607
608
  projectId?: number | null;
608
609
  projectAssignmentId?: number | null;
610
+ assigneeCollaboratorId?: number | null;
609
611
  name?: string | null;
610
612
  description?: string | null;
613
+ priority?: 'low' | 'medium' | 'high';
611
614
  status?: (typeof TASK_STATUS_VALUES)[number];
615
+ dueDate?: string | null;
616
+ estimateHours?: number | null;
617
+ position?: number;
618
+ tags?: string | null;
612
619
  };
613
620
 
614
621
  type QuickTimesheetEntryPayload = {
@@ -1743,6 +1750,28 @@ export class OperationsService {
1743
1750
  data.equityParticipation
1744
1751
  );
1745
1752
  }
1753
+
1754
+ if (
1755
+ data.compensationAmount !== undefined ||
1756
+ data.contractDescription !== undefined ||
1757
+ data.autoGenerateContractDraft !== undefined ||
1758
+ data.joinedAt !== undefined ||
1759
+ data.weeklyCapacityHours !== undefined ||
1760
+ data.supervisorCollaboratorId !== undefined ||
1761
+ data.collaboratorType !== undefined ||
1762
+ data.collaboratorTypeId !== undefined ||
1763
+ data.collaboratorTypeSlug !== undefined ||
1764
+ data.code !== undefined ||
1765
+ data.personId !== undefined ||
1766
+ data.displayName !== undefined
1767
+ ) {
1768
+ await this.syncHiringContractDraft(
1769
+ tx as any,
1770
+ actor.userId,
1771
+ collaboratorId,
1772
+ data
1773
+ );
1774
+ }
1746
1775
  });
1747
1776
 
1748
1777
  return this.getCollaboratorByIdForUser(userId, collaboratorId);
@@ -2002,8 +2031,8 @@ export class OperationsService {
2002
2031
  p.progress_percent AS "progressPercent",
2003
2032
  p.delivery_model AS "deliveryModel",
2004
2033
  p.budget_amount AS "budgetAmount",
2005
- p.start_date AS "startDate",
2006
- p.end_date AS "endDate",
2034
+ TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
2035
+ TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
2007
2036
  c.name AS "contractName",
2008
2037
  c.status AS "contractStatus",
2009
2038
  m.display_name AS "managerName",
@@ -2105,8 +2134,8 @@ export class OperationsService {
2105
2134
  MAX(pa.id)::int AS "projectAssignmentId",
2106
2135
  MAX(pa.role_label) AS "roleLabel",
2107
2136
  p.status,
2108
- p.start_date AS "startDate",
2109
- p.end_date AS "endDate"
2137
+ TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
2138
+ TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate"
2110
2139
  FROM operations_project_assignment pa
2111
2140
  JOIN operations_project p
2112
2141
  ON p.id = pa.project_id
@@ -2255,54 +2284,92 @@ export class OperationsService {
2255
2284
 
2256
2285
  async createTask(userId: number, data: TaskPayload) {
2257
2286
  const actor = await this.getActorContext(userId);
2258
- this.ensureCollaborator(actor);
2287
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
2288
+ throw new ForbiddenException(
2289
+ 'Operations collaborator access is required.'
2290
+ );
2291
+ }
2259
2292
  this.requireFields(data as Record<string, unknown>, ['name']);
2260
2293
 
2261
- if (!actor.collaboratorId) {
2262
- throw new BadRequestException('Collaborator context is required.');
2294
+ let assignmentId: number | null = null;
2295
+ let projectId: number | null = null;
2296
+
2297
+ if (data.projectId || data.projectAssignmentId) {
2298
+ const assignment = await this.resolveProjectAssignmentForActor(
2299
+ this.prisma,
2300
+ actor,
2301
+ {
2302
+ projectId: data.projectId ?? null,
2303
+ projectAssignmentId: data.projectAssignmentId ?? null,
2304
+ }
2305
+ );
2306
+ await this.assertProjectAccess(actor, assignment.projectId);
2307
+ assignmentId = assignment.id;
2308
+ projectId = assignment.projectId;
2309
+ } else if (data.projectId) {
2310
+ projectId = data.projectId;
2311
+ await this.assertProjectAccess(actor, projectId);
2312
+ } else {
2313
+ throw new BadRequestException('Either projectId or projectAssignmentId is required.');
2263
2314
  }
2264
2315
 
2265
- const assignment = await this.resolveOwnedProjectAssignment(
2266
- this.prisma,
2267
- actor.collaboratorId,
2268
- {
2269
- projectId: data.projectId ?? null,
2270
- projectAssignmentId: data.projectAssignmentId ?? null,
2271
- }
2272
- );
2273
- await this.assertProjectAccess(actor, assignment.projectId);
2316
+ if (!projectId) {
2317
+ projectId = data.projectId ?? null;
2318
+ }
2274
2319
 
2275
2320
  const name = this.normalizeOptionalText(data.name);
2276
2321
  if (!name) {
2277
2322
  throw new BadRequestException('Field "name" is required.');
2278
2323
  }
2279
2324
 
2325
+ const maxPositionRow = await this.querySingle<{ max_pos: number | null }>(
2326
+ `SELECT MAX(position) AS max_pos
2327
+ FROM operations_task
2328
+ WHERE project_id = $1
2329
+ AND status = $2::operations_task_status_574c143dbe_enum
2330
+ AND deleted_at IS NULL`,
2331
+ [projectId, data.status ?? 'todo']
2332
+ );
2333
+ const nextPosition = ((maxPositionRow?.max_pos ?? -1) as number) + 1;
2334
+
2280
2335
  const created = await this.querySingle<{ id: number }>(
2281
2336
  `INSERT INTO operations_task (
2337
+ project_id,
2282
2338
  project_assignment_id,
2339
+ assignee_collaborator_id,
2283
2340
  name,
2284
2341
  description,
2342
+ priority,
2285
2343
  status,
2344
+ due_date,
2345
+ estimate_hours,
2346
+ position,
2347
+ tags,
2286
2348
  created_at,
2287
2349
  updated_at
2288
2350
  ) VALUES (
2289
- $1,
2290
- $2,
2291
- $3,
2292
- $4,
2293
- NOW(),
2294
- NOW()
2351
+ $1, $2, $3, $4, $5,
2352
+ $6::operations_task_priority_394ab327eb_enum,
2353
+ $7::operations_task_status_574c143dbe_enum,
2354
+ $8::date, $9::decimal, $10, $11, NOW(), NOW()
2295
2355
  )
2296
2356
  RETURNING id`,
2297
2357
  [
2298
- assignment.id,
2358
+ projectId,
2359
+ assignmentId,
2360
+ data.assigneeCollaboratorId ?? null,
2299
2361
  name,
2300
2362
  this.normalizeOptionalText(data.description),
2301
- data.status ?? 'active',
2363
+ data.priority ?? 'medium',
2364
+ data.status ?? 'todo',
2365
+ data.dueDate ?? null,
2366
+ data.estimateHours ?? null,
2367
+ data.position ?? nextPosition,
2368
+ data.tags ?? null,
2302
2369
  ]
2303
2370
  );
2304
2371
 
2305
- return this.getTaskOptionById(actor.collaboratorId, created?.id ?? 0);
2372
+ return this.getProjectBoardTask(created?.id ?? 0);
2306
2373
  }
2307
2374
 
2308
2375
  async updateTask(
@@ -2311,15 +2378,15 @@ export class OperationsService {
2311
2378
  data: Partial<TaskPayload>
2312
2379
  ) {
2313
2380
  const actor = await this.getActorContext(userId);
2314
- this.ensureCollaborator(actor);
2315
-
2316
- if (!actor.collaboratorId) {
2317
- throw new BadRequestException('Collaborator context is required.');
2381
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
2382
+ throw new ForbiddenException(
2383
+ 'Operations collaborator access is required.'
2384
+ );
2318
2385
  }
2319
2386
 
2320
- const current = await this.getOwnedTaskRecord(
2387
+ const current = await this.getTaskRecordForActor(
2321
2388
  this.prisma,
2322
- actor.collaboratorId,
2389
+ actor,
2323
2390
  taskId
2324
2391
  );
2325
2392
  await this.assertProjectAccess(actor, current.projectId);
@@ -2334,56 +2401,72 @@ export class OperationsService {
2334
2401
  }
2335
2402
 
2336
2403
  await this.prisma.$transaction(async (tx) => {
2337
- const nextAssignment =
2338
- data.projectId !== undefined || data.projectAssignmentId !== undefined
2339
- ? await this.resolveOwnedProjectAssignment(
2340
- tx as any,
2341
- actor.collaboratorId as number,
2342
- {
2343
- projectId: data.projectId ?? null,
2344
- projectAssignmentId: data.projectAssignmentId ?? null,
2345
- }
2346
- )
2347
- : {
2348
- id: current.projectAssignmentId,
2349
- projectId: current.projectId,
2350
- };
2404
+ let nextAssignmentId = current.projectAssignmentId;
2405
+ let nextProjectId = current.projectId;
2351
2406
 
2352
- await this.assertProjectAccess(actor, nextAssignment.projectId);
2407
+ if (data.projectId !== undefined || data.projectAssignmentId !== undefined) {
2408
+ const nextAssignment = await this.resolveProjectAssignmentForActor(
2409
+ tx as any,
2410
+ actor,
2411
+ {
2412
+ projectId: data.projectId ?? null,
2413
+ projectAssignmentId: data.projectAssignmentId ?? null,
2414
+ }
2415
+ );
2416
+ await this.assertProjectAccess(actor, nextAssignment.projectId);
2417
+ nextAssignmentId = nextAssignment.id;
2418
+ nextProjectId = nextAssignment.projectId;
2419
+ }
2353
2420
 
2354
2421
  await (tx as any).$executeRawUnsafe(
2355
2422
  `UPDATE operations_task
2356
- SET project_assignment_id = $1,
2357
- name = $2,
2358
- description = $3,
2359
- status = $4,
2423
+ SET project_id = $1,
2424
+ project_assignment_id = $2,
2425
+ assignee_collaborator_id = $3,
2426
+ name = $4,
2427
+ description = $5,
2428
+ priority = $6::operations_task_priority_394ab327eb_enum,
2429
+ status = $7::operations_task_status_574c143dbe_enum,
2430
+ due_date = $8::date,
2431
+ estimate_hours = $9::decimal,
2432
+ position = $10,
2433
+ tags = $11,
2360
2434
  updated_at = NOW()
2361
- WHERE id = $5
2435
+ WHERE id = $12
2362
2436
  AND deleted_at IS NULL`,
2363
- nextAssignment.id,
2437
+ nextProjectId,
2438
+ nextAssignmentId,
2439
+ data.assigneeCollaboratorId !== undefined
2440
+ ? (data.assigneeCollaboratorId ?? null)
2441
+ : current.assigneeCollaboratorId,
2364
2442
  nextName,
2365
2443
  data.description !== undefined
2366
2444
  ? this.normalizeOptionalText(data.description)
2367
2445
  : (current.description ?? null),
2446
+ data.priority ?? current.priority,
2368
2447
  data.status ?? current.status,
2448
+ data.dueDate !== undefined ? (data.dueDate ?? null) : current.dueDate,
2449
+ data.estimateHours !== undefined ? (data.estimateHours ?? null) : current.estimateHours,
2450
+ data.position !== undefined ? data.position : current.position,
2451
+ data.tags !== undefined ? (data.tags ?? null) : current.tags,
2369
2452
  taskId
2370
2453
  );
2371
2454
  });
2372
2455
 
2373
- return this.getTaskOptionById(actor.collaboratorId, taskId);
2456
+ return this.getProjectBoardTask(taskId);
2374
2457
  }
2375
2458
 
2376
2459
  async removeTask(userId: number, taskId: number) {
2377
2460
  const actor = await this.getActorContext(userId);
2378
- this.ensureCollaborator(actor);
2379
-
2380
- if (!actor.collaboratorId) {
2381
- throw new BadRequestException('Collaborator context is required.');
2461
+ if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
2462
+ throw new ForbiddenException(
2463
+ 'Operations collaborator access is required.'
2464
+ );
2382
2465
  }
2383
2466
 
2384
- const current = await this.getOwnedTaskRecord(
2467
+ const current = await this.getTaskRecordForActor(
2385
2468
  this.prisma,
2386
- actor.collaboratorId,
2469
+ actor,
2387
2470
  taskId
2388
2471
  );
2389
2472
  await this.assertProjectAccess(actor, current.projectId);
@@ -2392,7 +2475,6 @@ export class OperationsService {
2392
2475
  await (tx as any).$executeRawUnsafe(
2393
2476
  `UPDATE operations_task
2394
2477
  SET deleted_at = COALESCE(deleted_at, NOW()),
2395
- status = 'archived',
2396
2478
  updated_at = NOW()
2397
2479
  WHERE id = $1
2398
2480
  AND deleted_at IS NULL`,
@@ -2732,6 +2814,7 @@ export class OperationsService {
2732
2814
  `INSERT INTO operations_project (
2733
2815
  contract_id,
2734
2816
  manager_collaborator_id,
2817
+ client_person_id,
2735
2818
  code,
2736
2819
  name,
2737
2820
  client_name,
@@ -2745,15 +2828,16 @@ export class OperationsService {
2745
2828
  created_at,
2746
2829
  updated_at
2747
2830
  ) VALUES (
2748
- $1, $2, $3, $4, $5, $6,
2749
- $7::operations_project_status_965e8d4b2d_enum,
2750
- $8,
2751
- $9::operations_project_delivery_model_75ee11b3b7_enum,
2752
- $10, $11::date, $12::date, NOW(), NOW()
2831
+ $1, $2, $3, $4, $5, $6, $7,
2832
+ $8::operations_project_status_965e8d4b2d_enum,
2833
+ $9,
2834
+ $10::operations_project_delivery_model_75ee11b3b7_enum,
2835
+ $11, $12::date, $13::date, NOW(), NOW()
2753
2836
  )
2754
2837
  RETURNING id`,
2755
2838
  data.contractId ?? null,
2756
2839
  data.managerCollaboratorId ?? null,
2840
+ data.clientPersonId ?? null,
2757
2841
  data.code,
2758
2842
  data.name,
2759
2843
  data.clientName ?? null,
@@ -2823,6 +2907,7 @@ export class OperationsService {
2823
2907
  const params: unknown[] = [];
2824
2908
  this.pushUpdate(updates, params, 'contract_id', data.contractId);
2825
2909
  this.pushUpdate(updates, params, 'manager_collaborator_id', data.managerCollaboratorId);
2910
+ this.pushUpdate(updates, params, 'client_person_id', data.clientPersonId);
2826
2911
  this.pushUpdate(updates, params, 'code', data.code);
2827
2912
  this.pushUpdate(updates, params, 'name', data.name);
2828
2913
  this.pushUpdate(updates, params, 'client_name', data.clientName);
@@ -2839,7 +2924,7 @@ export class OperationsService {
2839
2924
  updates,
2840
2925
  params,
2841
2926
  'delivery_model',
2842
- data.deliveryModel,
2927
+ (data.deliveryModel as string | null | undefined) === '' ? null : data.deliveryModel,
2843
2928
  'operations_project_delivery_model_75ee11b3b7_enum'
2844
2929
  );
2845
2930
  this.pushUpdate(updates, params, 'budget_amount', data.budgetAmount);
@@ -2869,7 +2954,7 @@ export class OperationsService {
2869
2954
  const nextContractId =
2870
2955
  data.contractId !== undefined
2871
2956
  ? data.contractId
2872
- : (currentProject.contractId ?? null);
2957
+ : (currentProject.relatedContract?.id ?? null);
2873
2958
  const shouldGenerateDraft =
2874
2959
  !nextContractId && data.autoGenerateContractDraft === true;
2875
2960
 
@@ -2922,6 +3007,39 @@ export class OperationsService {
2922
3007
  contractId,
2923
3008
  projectId
2924
3009
  );
3010
+ } else if (
3011
+ nextContractId &&
3012
+ (data.monthlyHourCap !== undefined || data.billingModel !== undefined)
3013
+ ) {
3014
+ const contractUpdates: string[] = [];
3015
+ const contractParams: unknown[] = [];
3016
+
3017
+ this.pushUpdate(
3018
+ contractUpdates,
3019
+ contractParams,
3020
+ 'monthly_hour_cap',
3021
+ data.monthlyHourCap
3022
+ );
3023
+ this.pushUpdate(
3024
+ contractUpdates,
3025
+ contractParams,
3026
+ 'billing_model',
3027
+ (data.billingModel as string | null | undefined) === ''
3028
+ ? null
3029
+ : data.billingModel,
3030
+ 'operations_contract_billing_model_409dc7fea2_enum'
3031
+ );
3032
+
3033
+ if (contractUpdates.length) {
3034
+ contractParams.push(nextContractId);
3035
+ await (tx as any).$executeRawUnsafe(
3036
+ `UPDATE operations_contract
3037
+ SET ${contractUpdates.join(', ')},
3038
+ updated_at = NOW()
3039
+ WHERE id = $${contractParams.length}`,
3040
+ ...contractParams
3041
+ );
3042
+ }
2925
3043
  }
2926
3044
  });
2927
3045
 
@@ -6608,6 +6726,8 @@ export class OperationsService {
6608
6726
  id: number;
6609
6727
  contractId: number | null;
6610
6728
  managerCollaboratorId: number | null;
6729
+ clientPersonId: number | null;
6730
+ clientAvatarId: number | null;
6611
6731
  code: string;
6612
6732
  name: string;
6613
6733
  clientName: string | null;
@@ -6629,6 +6749,8 @@ export class OperationsService {
6629
6749
  `SELECT p.id,
6630
6750
  p.contract_id AS "contractId",
6631
6751
  p.manager_collaborator_id AS "managerCollaboratorId",
6752
+ p.client_person_id AS "clientPersonId",
6753
+ client_person.avatar_id AS "clientAvatarId",
6632
6754
  p.code,
6633
6755
  p.name,
6634
6756
  p.client_name AS "clientName",
@@ -6637,8 +6759,8 @@ export class OperationsService {
6637
6759
  p.progress_percent AS "progressPercent",
6638
6760
  p.delivery_model AS "deliveryModel",
6639
6761
  p.budget_amount AS "budgetAmount",
6640
- p.start_date AS "startDate",
6641
- p.end_date AS "endDate",
6762
+ TO_CHAR(p.start_date, 'YYYY-MM-DD') AS "startDate",
6763
+ TO_CHAR(p.end_date, 'YYYY-MM-DD') AS "endDate",
6642
6764
  c.name AS "contractName",
6643
6765
  c.status AS "contractStatus",
6644
6766
  c.contract_category AS "contractCategory",
@@ -6649,6 +6771,7 @@ export class OperationsService {
6649
6771
  FROM operations_project p
6650
6772
  LEFT JOIN operations_contract c ON c.id = p.contract_id
6651
6773
  LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
6774
+ LEFT JOIN person client_person ON client_person.id = p.client_person_id
6652
6775
  LEFT JOIN operations_project_assignment pa
6653
6776
  ON pa.project_id = p.id
6654
6777
  AND pa.deleted_at IS NULL
@@ -6664,7 +6787,7 @@ export class OperationsService {
6664
6787
  ) project_role_locale ON TRUE
6665
6788
  WHERE p.id = $1
6666
6789
  AND p.deleted_at IS NULL
6667
- GROUP BY p.id, c.id, m.id`,
6790
+ GROUP BY p.id, c.id, m.id, client_person.id`,
6668
6791
  [projectId, actorCollaboratorId ?? null]
6669
6792
  );
6670
6793
 
@@ -6677,6 +6800,9 @@ export class OperationsService {
6677
6800
  this.queryRows<{
6678
6801
  id: number;
6679
6802
  collaboratorId: number;
6803
+ userId: number | null;
6804
+ personAvatarId: number | null;
6805
+ userPhotoId: number | null;
6680
6806
  collaboratorName: string;
6681
6807
  projectRoleId: number | null;
6682
6808
  roleLabel: string | null;
@@ -6689,17 +6815,22 @@ export class OperationsService {
6689
6815
  }>(
6690
6816
  `SELECT pa.id,
6691
6817
  pa.collaborator_id AS "collaboratorId",
6818
+ c.user_id AS "userId",
6819
+ person_record.avatar_id AS "personAvatarId",
6820
+ collaborator_user.photo_id AS "userPhotoId",
6692
6821
  c.display_name AS "collaboratorName",
6693
6822
  pa.project_role_id AS "projectRoleId",
6694
6823
  COALESCE(project_role_locale.name, pa.role_label) AS "roleLabel",
6695
6824
  pa.allocation_percent AS "allocationPercent",
6696
6825
  pa.weekly_hours AS "weeklyHours",
6697
6826
  pa.is_billable AS "isBillable",
6698
- pa.start_date AS "startDate",
6699
- pa.end_date AS "endDate",
6827
+ TO_CHAR(pa.start_date, 'YYYY-MM-DD') AS "startDate",
6828
+ TO_CHAR(pa.end_date, 'YYYY-MM-DD') AS "endDate",
6700
6829
  pa.status
6701
6830
  FROM operations_project_assignment pa
6702
6831
  JOIN operations_collaborator c ON c.id = pa.collaborator_id
6832
+ LEFT JOIN person person_record ON person_record.id = c.person_id
6833
+ LEFT JOIN "user" collaborator_user ON collaborator_user.id = c.user_id
6703
6834
  LEFT JOIN operations_project_role project_role
6704
6835
  ON project_role.id = pa.project_role_id
6705
6836
  AND project_role.deleted_at IS NULL
@@ -6739,8 +6870,8 @@ export class OperationsService {
6739
6870
  contract_category AS "contractCategory",
6740
6871
  billing_model AS "billingModel",
6741
6872
  status,
6742
- start_date AS "startDate",
6743
- end_date AS "endDate",
6873
+ TO_CHAR(start_date, 'YYYY-MM-DD') AS "startDate",
6874
+ TO_CHAR(end_date, 'YYYY-MM-DD') AS "endDate",
6744
6875
  budget_amount AS "budgetAmount",
6745
6876
  monthly_hour_cap AS "monthlyHourCap",
6746
6877
  description,
@@ -6951,7 +7082,8 @@ export class OperationsService {
6951
7082
  FROM operations_contract c
6952
7083
  WHERE c.related_collaborator_id = $1
6953
7084
  AND c.deleted_at IS NULL
6954
- ORDER BY c.created_at DESC`,
7085
+ ORDER BY CASE WHEN c.origin_type = 'employee_hiring' THEN 0 ELSE 1 END,
7086
+ c.created_at DESC`,
6955
7087
  [collaboratorId]
6956
7088
  ),
6957
7089
  this.queryRows(
@@ -7137,6 +7269,69 @@ export class OperationsService {
7137
7269
  return assignment[0];
7138
7270
  }
7139
7271
 
7272
+ private async resolveProjectAssignmentForActor(
7273
+ client: any,
7274
+ actor: ActorContext,
7275
+ input: {
7276
+ projectId?: number | null;
7277
+ projectAssignmentId?: number | null;
7278
+ }
7279
+ ) {
7280
+ if (actor.collaboratorId && !actor.isDirector && !actor.isSupervisor) {
7281
+ return this.resolveOwnedProjectAssignment(
7282
+ client,
7283
+ actor.collaboratorId,
7284
+ input
7285
+ );
7286
+ }
7287
+
7288
+ if (!input.projectId && !input.projectAssignmentId) {
7289
+ throw new BadRequestException(
7290
+ 'Either projectId or projectAssignmentId is required.'
7291
+ );
7292
+ }
7293
+
7294
+ const params: unknown[] = [];
7295
+ const filters = ['pa.deleted_at IS NULL', 'p.deleted_at IS NULL'];
7296
+
7297
+ if (input.projectAssignmentId) {
7298
+ filters.push(`pa.id = ${this.param(params, input.projectAssignmentId)}`);
7299
+ }
7300
+
7301
+ if (input.projectId) {
7302
+ filters.push(`pa.project_id = ${this.param(params, input.projectId)}`);
7303
+ }
7304
+
7305
+ const assignment = (await client.$queryRawUnsafe(
7306
+ `SELECT pa.id,
7307
+ pa.project_id AS "projectId",
7308
+ p.name AS "projectName",
7309
+ p.code AS "projectCode",
7310
+ pa.role_label AS "roleLabel"
7311
+ FROM operations_project_assignment pa
7312
+ JOIN operations_project p
7313
+ ON p.id = pa.project_id
7314
+ WHERE ${filters.join(' AND ')}
7315
+ ORDER BY CASE WHEN pa.status = 'active' THEN 0 ELSE 1 END,
7316
+ pa.start_date DESC NULLS LAST,
7317
+ pa.id DESC
7318
+ LIMIT 1`,
7319
+ ...params
7320
+ )) as Array<{
7321
+ id: number;
7322
+ projectId: number;
7323
+ projectName: string;
7324
+ projectCode: string | null;
7325
+ roleLabel: string | null;
7326
+ }>;
7327
+
7328
+ if (!assignment[0]) {
7329
+ throw new NotFoundException('Project assignment not found.');
7330
+ }
7331
+
7332
+ return assignment[0];
7333
+ }
7334
+
7140
7335
  private async getOwnedTaskRecord(
7141
7336
  client: any,
7142
7337
  collaboratorId: number,
@@ -7146,21 +7341,33 @@ export class OperationsService {
7146
7341
  `SELECT t.id,
7147
7342
  t.name,
7148
7343
  t.description,
7344
+ t.priority,
7149
7345
  t.status,
7346
+ t.due_date AS "dueDate",
7347
+ t.estimate_hours AS "estimateHours",
7348
+ t.position,
7349
+ t.tags,
7350
+ t.assignee_collaborator_id AS "assigneeCollaboratorId",
7150
7351
  t.project_assignment_id AS "projectAssignmentId",
7151
- pa.project_id AS "projectId",
7352
+ COALESCE(t.project_id, pa.project_id) AS "projectId",
7152
7353
  p.name AS "projectName",
7153
7354
  p.code AS "projectCode"
7154
7355
  FROM operations_task t
7155
- JOIN operations_project_assignment pa
7356
+ LEFT JOIN operations_project_assignment pa
7156
7357
  ON pa.id = t.project_assignment_id
7157
7358
  AND pa.deleted_at IS NULL
7158
- JOIN operations_project p
7159
- ON p.id = pa.project_id
7359
+ LEFT JOIN operations_project p
7360
+ ON p.id = COALESCE(t.project_id, pa.project_id)
7160
7361
  AND p.deleted_at IS NULL
7161
7362
  WHERE t.id = $1
7162
7363
  AND t.deleted_at IS NULL
7163
- AND pa.collaborator_id = $2
7364
+ AND (
7365
+ pa.collaborator_id = $2
7366
+ OR t.project_id IN (
7367
+ SELECT pa2.project_id FROM operations_project_assignment pa2
7368
+ WHERE pa2.collaborator_id = $2 AND pa2.deleted_at IS NULL
7369
+ )
7370
+ )
7164
7371
  LIMIT 1`,
7165
7372
  taskId,
7166
7373
  collaboratorId
@@ -7168,8 +7375,14 @@ export class OperationsService {
7168
7375
  id: number;
7169
7376
  name: string;
7170
7377
  description: string | null;
7378
+ priority: string;
7171
7379
  status: string;
7172
- projectAssignmentId: number;
7380
+ dueDate: string | null;
7381
+ estimateHours: number | null;
7382
+ position: number;
7383
+ tags: string | null;
7384
+ assigneeCollaboratorId: number | null;
7385
+ projectAssignmentId: number | null;
7173
7386
  projectId: number;
7174
7387
  projectName: string;
7175
7388
  projectCode: string | null;
@@ -7184,12 +7397,127 @@ export class OperationsService {
7184
7397
  return task[0];
7185
7398
  }
7186
7399
 
7187
- private async getTaskOptionById(collaboratorId: number, taskId: number) {
7188
- const task = await this.getOwnedTaskRecord(this.prisma, collaboratorId, taskId);
7189
- return {
7190
- ...task,
7191
- label: [task.name, task.projectName].filter(Boolean).join(' • '),
7192
- };
7400
+ private async getTaskRecordForActor(
7401
+ client: any,
7402
+ actor: ActorContext,
7403
+ taskId: number
7404
+ ) {
7405
+ if (actor.collaboratorId && !actor.isDirector && !actor.isSupervisor) {
7406
+ return this.getOwnedTaskRecord(client, actor.collaboratorId, taskId);
7407
+ }
7408
+
7409
+ const task = await this.getProjectBoardTask(taskId);
7410
+ if (!task?.projectId) {
7411
+ throw new NotFoundException('Task not found.');
7412
+ }
7413
+
7414
+ return task;
7415
+ }
7416
+
7417
+ async listProjectBoardTasks(userId: number, projectId: number) {
7418
+ const actor = await this.getActorContext(userId);
7419
+ this.ensureCollaborator(actor);
7420
+ await this.assertProjectAccess(actor, projectId);
7421
+
7422
+ const rows = await this.queryRows<{
7423
+ id: number;
7424
+ name: string;
7425
+ description: string | null;
7426
+ priority: string;
7427
+ status: string;
7428
+ dueDate: string | null;
7429
+ estimateHours: number | null;
7430
+ position: number;
7431
+ tags: string | null;
7432
+ assigneeCollaboratorId: number | null;
7433
+ assigneeName: string | null;
7434
+ assigneeUserPhotoId: number | null;
7435
+ assigneePersonAvatarId: number | null;
7436
+ projectAssignmentId: number | null;
7437
+ createdAt: string;
7438
+ }>(
7439
+ `SELECT t.id,
7440
+ t.name,
7441
+ t.description,
7442
+ t.priority,
7443
+ t.status,
7444
+ t.due_date AS "dueDate",
7445
+ t.estimate_hours AS "estimateHours",
7446
+ t.position,
7447
+ t.tags,
7448
+ t.assignee_collaborator_id AS "assigneeCollaboratorId",
7449
+ ac.display_name AS "assigneeName",
7450
+ au.photo_id AS "assigneeUserPhotoId",
7451
+ ap.avatar_id AS "assigneePersonAvatarId",
7452
+ t.project_assignment_id AS "projectAssignmentId",
7453
+ t.created_at AS "createdAt"
7454
+ FROM operations_task t
7455
+ LEFT JOIN operations_collaborator ac
7456
+ ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
7457
+ LEFT JOIN "user" au
7458
+ ON au.id = ac.user_id
7459
+ LEFT JOIN person ap
7460
+ ON ap.id = ac.person_id
7461
+ WHERE COALESCE(t.project_id, (
7462
+ SELECT pa.project_id FROM operations_project_assignment pa
7463
+ WHERE pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
7464
+ LIMIT 1
7465
+ )) = $1
7466
+ AND t.deleted_at IS NULL
7467
+ ORDER BY t.status ASC, t.position ASC, t.id ASC`,
7468
+ [projectId]
7469
+ );
7470
+
7471
+ return rows;
7472
+ }
7473
+
7474
+ private async getProjectBoardTask(taskId: number) {
7475
+ const rows = await this.queryRows<{
7476
+ id: number;
7477
+ name: string;
7478
+ description: string | null;
7479
+ priority: string;
7480
+ status: string;
7481
+ dueDate: string | null;
7482
+ estimateHours: number | null;
7483
+ position: number;
7484
+ tags: string | null;
7485
+ assigneeCollaboratorId: number | null;
7486
+ assigneeName: string | null;
7487
+ assigneeUserPhotoId: number | null;
7488
+ assigneePersonAvatarId: number | null;
7489
+ projectAssignmentId: number | null;
7490
+ projectId: number | null;
7491
+ createdAt: string;
7492
+ }>(
7493
+ `SELECT t.id,
7494
+ t.name,
7495
+ t.description,
7496
+ t.priority,
7497
+ t.status,
7498
+ t.due_date AS "dueDate",
7499
+ t.estimate_hours AS "estimateHours",
7500
+ t.position,
7501
+ t.tags,
7502
+ t.assignee_collaborator_id AS "assigneeCollaboratorId",
7503
+ ac.display_name AS "assigneeName",
7504
+ au.photo_id AS "assigneeUserPhotoId",
7505
+ ap.avatar_id AS "assigneePersonAvatarId",
7506
+ t.project_assignment_id AS "projectAssignmentId",
7507
+ COALESCE(t.project_id, pa.project_id) AS "projectId",
7508
+ t.created_at AS "createdAt"
7509
+ FROM operations_task t
7510
+ LEFT JOIN operations_collaborator ac
7511
+ ON ac.id = t.assignee_collaborator_id AND ac.deleted_at IS NULL
7512
+ LEFT JOIN "user" au ON au.id = ac.user_id
7513
+ LEFT JOIN person ap ON ap.id = ac.person_id
7514
+ LEFT JOIN operations_project_assignment pa
7515
+ ON pa.id = t.project_assignment_id AND pa.deleted_at IS NULL
7516
+ WHERE t.id = $1`,
7517
+ [taskId]
7518
+ );
7519
+
7520
+ return rows[0] ?? null;
7193
7521
  }
7194
7522
 
7195
7523
  private async getOrCreateTimesheetForWorkDate(
@@ -8055,6 +8383,146 @@ export class OperationsService {
8055
8383
  );
8056
8384
  }
8057
8385
 
8386
+ private async syncHiringContractDraft(
8387
+ client: any,
8388
+ updatedByUserId: number,
8389
+ collaboratorId: number,
8390
+ data: Partial<CollaboratorPayload>
8391
+ ) {
8392
+ const collaborator = await client.$queryRawUnsafe(
8393
+ `SELECT c.id,
8394
+ c.code,
8395
+ COALESCE(NULLIF(c.display_name, ''), person_record.name) AS "displayName",
8396
+ collaborator_type.slug AS "collaboratorTypeSlug",
8397
+ c.supervisor_collaborator_id AS "supervisorCollaboratorId",
8398
+ c.joined_at AS "joinedAt",
8399
+ c.weekly_capacity_hours AS "weeklyCapacityHours"
8400
+ FROM operations_collaborator c
8401
+ LEFT JOIN person person_record
8402
+ ON person_record.id = c.person_id
8403
+ LEFT JOIN operations_collaborator_type collaborator_type
8404
+ ON collaborator_type.id = c.collaborator_type_id
8405
+ AND collaborator_type.deleted_at IS NULL
8406
+ WHERE c.id = $1
8407
+ AND c.deleted_at IS NULL
8408
+ LIMIT 1`,
8409
+ collaboratorId
8410
+ ) as Array<{
8411
+ id: number;
8412
+ code: string | null;
8413
+ displayName: string | null;
8414
+ collaboratorTypeSlug: string | null;
8415
+ supervisorCollaboratorId: number | null;
8416
+ joinedAt: string | null;
8417
+ weeklyCapacityHours: number | null;
8418
+ }>;
8419
+
8420
+ const currentCollaborator = collaborator[0] ?? null;
8421
+
8422
+ if (!currentCollaborator) {
8423
+ throw new NotFoundException('Collaborator not found.');
8424
+ }
8425
+
8426
+ const hiringContracts = (await client.$queryRawUnsafe(
8427
+ `SELECT id,
8428
+ budget_amount AS "budgetAmount",
8429
+ description
8430
+ FROM operations_contract
8431
+ WHERE related_collaborator_id = $1
8432
+ AND origin_type = 'employee_hiring'
8433
+ AND deleted_at IS NULL
8434
+ ORDER BY created_at DESC
8435
+ LIMIT 1`,
8436
+ collaboratorId
8437
+ )) as Array<{
8438
+ id: number;
8439
+ budgetAmount: number | null;
8440
+ description: string | null;
8441
+ }>;
8442
+
8443
+ const hiringContract = hiringContracts[0] ?? null;
8444
+ const collaboratorCode =
8445
+ this.normalizeOptionalText(currentCollaborator.code) ??
8446
+ `COL-${collaboratorId}`;
8447
+ const displayName =
8448
+ this.normalizeOptionalText(currentCollaborator.displayName) ??
8449
+ `Collaborator ${collaboratorId}`;
8450
+ const collaboratorTypeSlug =
8451
+ this.normalizeOptionalText(currentCollaborator.collaboratorTypeSlug) ??
8452
+ 'other';
8453
+ const startDate =
8454
+ data.joinedAt !== undefined
8455
+ ? data.joinedAt ?? null
8456
+ : currentCollaborator.joinedAt ?? null;
8457
+ const weeklyCapacityHours =
8458
+ data.weeklyCapacityHours !== undefined
8459
+ ? data.weeklyCapacityHours ?? null
8460
+ : currentCollaborator.weeklyCapacityHours ?? null;
8461
+ const compensationAmount =
8462
+ data.compensationAmount !== undefined
8463
+ ? data.compensationAmount ?? null
8464
+ : hiringContract?.budgetAmount ?? null;
8465
+ const description =
8466
+ data.contractDescription !== undefined
8467
+ ? this.normalizeOptionalText(data.contractDescription)
8468
+ : hiringContract?.description ?? null;
8469
+ const supervisorCollaboratorId =
8470
+ data.supervisorCollaboratorId !== undefined
8471
+ ? data.supervisorCollaboratorId ?? null
8472
+ : currentCollaborator.supervisorCollaboratorId ?? null;
8473
+
8474
+ if (!hiringContract) {
8475
+ if (data.autoGenerateContractDraft === false) {
8476
+ return;
8477
+ }
8478
+
8479
+ await this.createHiringContractDraft(client, updatedByUserId, {
8480
+ collaboratorId,
8481
+ collaboratorCode,
8482
+ displayName,
8483
+ collaboratorType: collaboratorTypeSlug,
8484
+ supervisorCollaboratorId,
8485
+ startDate,
8486
+ weeklyCapacityHours,
8487
+ compensationAmount,
8488
+ description,
8489
+ });
8490
+ return;
8491
+ }
8492
+
8493
+ await client.$executeRawUnsafe(
8494
+ `UPDATE operations_contract
8495
+ SET code = $1,
8496
+ name = $2,
8497
+ contract_category = $3::operations_contract_contract_category_70d553ea09_enum,
8498
+ contract_type = $4::operations_contract_contract_type_48331e2ebf_enum,
8499
+ client_name = $5,
8500
+ billing_model = $6::operations_contract_billing_model_409dc7fea2_enum,
8501
+ account_manager_collaborator_id = $7,
8502
+ start_date = $8::date,
8503
+ effective_date = $8::date,
8504
+ budget_amount = $9,
8505
+ monthly_hour_cap = $10,
8506
+ description = $11,
8507
+ updated_by_user_id = $12,
8508
+ updated_at = NOW()
8509
+ WHERE id = $13`,
8510
+ `HIR-${collaboratorCode}`,
8511
+ this.buildHiringContractName(displayName, collaboratorTypeSlug),
8512
+ this.mapContractCategoryForCollaboratorType(collaboratorTypeSlug),
8513
+ this.mapContractTypeForCollaboratorType(collaboratorTypeSlug),
8514
+ displayName,
8515
+ this.mapBillingModelForCollaboratorType(collaboratorTypeSlug),
8516
+ supervisorCollaboratorId,
8517
+ startDate ?? new Date().toISOString().slice(0, 10),
8518
+ compensationAmount,
8519
+ weeklyCapacityHours ? Math.round(Number(weeklyCapacityHours) * 4) : null,
8520
+ description,
8521
+ updatedByUserId,
8522
+ hiringContract.id
8523
+ );
8524
+ }
8525
+
8058
8526
  private async createProjectContractDraft(
8059
8527
  client: any,
8060
8528
  createdByUserId: number,