@hed-hog/operations 0.0.325 → 0.0.327

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/dist/controllers/operations-collaborators.controller.d.ts +5 -0
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/operations.service.d.ts +9 -1
  4. package/dist/operations.service.d.ts.map +1 -1
  5. package/dist/operations.service.js +140 -26
  6. package/dist/operations.service.js.map +1 -1
  7. package/hedhog/data/integration_event_catalog.yaml +313 -0
  8. package/hedhog/data/setting_group.yaml +21 -0
  9. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +410 -23
  10. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +504 -375
  11. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +258 -230
  12. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +225 -162
  13. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +484 -230
  14. package/hedhog/frontend/app/_lib/api.ts.ejs +13 -4
  15. package/hedhog/frontend/app/_lib/hooks/use-mention-items.ts.ejs +28 -0
  16. package/hedhog/frontend/app/_lib/types.ts.ejs +30 -29
  17. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +347 -236
  18. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +31 -7
  19. package/hedhog/frontend/messages/en.json +38 -55
  20. package/hedhog/frontend/messages/en.json.ejs +21 -4
  21. package/hedhog/frontend/messages/pt.json +36 -55
  22. package/hedhog/frontend/messages/pt.json.ejs +14 -3
  23. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts +1 -0
  24. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts.map +1 -1
  25. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.ts +1 -0
  26. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts +1 -0
  27. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts.map +1 -1
  28. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.ts +1 -0
  29. package/hedhog/table/operations_collaborator.yaml +5 -0
  30. package/hedhog/table/operations_collaborator_compensation_history.yaml +4 -0
  31. package/package.json +5 -5
  32. package/src/operations.service.ts +202 -26
@@ -143,6 +143,9 @@ type CollaboratorPayload = {
143
143
  joinedAt?: string | null;
144
144
  leftAt?: string | null;
145
145
  compensationAmount?: number | null;
146
+ hourlyRate?: number | null;
147
+ compensationEffectiveDate?: string | null;
148
+ compensationNotes?: string | null;
146
149
  contractDescription?: string | null;
147
150
  autoGenerateContractDraft?: boolean;
148
151
  weeklySchedule?: Array<{
@@ -1980,14 +1983,42 @@ export class OperationsService {
1980
1983
  createdCollaboratorId,
1981
1984
  Number(data.compensationAmount),
1982
1985
  actor.userId,
1983
- null
1986
+ data.compensationNotes ?? null,
1987
+ data.compensationEffectiveDate ?? null
1988
+ );
1989
+ }
1990
+
1991
+ if (data.hourlyRate != null) {
1992
+ await (tx as any).$executeRawUnsafe(
1993
+ `UPDATE operations_collaborator SET hourly_rate = $1 WHERE id = $2`,
1994
+ Number(data.hourlyRate),
1995
+ createdCollaboratorId
1996
+ );
1997
+ await this.insertCollaboratorCompensationHistory(
1998
+ tx as any,
1999
+ createdCollaboratorId,
2000
+ Number(data.hourlyRate),
2001
+ actor.userId,
2002
+ data.compensationNotes ?? null,
2003
+ data.compensationEffectiveDate ?? null,
2004
+ 'hourly_rate'
1984
2005
  );
1985
2006
  }
1986
2007
 
1987
2008
  return createdCollaboratorId;
1988
2009
  });
1989
2010
 
1990
- return this.getCollaboratorByIdForUser(userId, collaboratorId);
2011
+ const result = await this.getCollaboratorByIdForUser(userId, collaboratorId);
2012
+
2013
+ await this.integrationApi.publishEvent({
2014
+ eventName: 'operations.collaborator.created',
2015
+ sourceModule: 'operations',
2016
+ aggregateType: 'collaborator',
2017
+ aggregateId: String(collaboratorId),
2018
+ payload: { id: collaboratorId, displayName: resolvedDisplayName, status: normalizedStatus },
2019
+ }).catch(() => null);
2020
+
2021
+ return result;
1991
2022
  }
1992
2023
 
1993
2024
  async updateCollaborator(
@@ -2023,6 +2054,7 @@ export class OperationsService {
2023
2054
  }
2024
2055
  this.pushUpdate(updates, params, 'level_label', data.levelLabel);
2025
2056
  this.pushUpdate(updates, params, 'weekly_capacity_hours', data.weeklyCapacityHours);
2057
+ this.pushUpdate(updates, params, 'hourly_rate', data.hourlyRate);
2026
2058
  this.pushUpdate(
2027
2059
  updates,
2028
2060
  params,
@@ -2034,7 +2066,18 @@ export class OperationsService {
2034
2066
  this.pushUpdate(updates, params, 'left_at', data.leftAt, 'date');
2035
2067
  this.pushUpdate(updates, params, 'notes', data.notes);
2036
2068
 
2069
+ let currentHourlyRate: number | null = null;
2070
+
2037
2071
  await this.prisma.$transaction(async (tx) => {
2072
+ if (data.hourlyRate !== undefined && data.hourlyRate !== null) {
2073
+ const curr = (await (tx as any).$queryRawUnsafe(
2074
+ `SELECT hourly_rate AS "hourlyRate" FROM operations_collaborator WHERE id = $1`,
2075
+ collaboratorId
2076
+ )) as { hourlyRate: string | null }[];
2077
+ currentHourlyRate =
2078
+ curr[0]?.hourlyRate != null ? Number(curr[0].hourlyRate) : null;
2079
+ }
2080
+
2038
2081
  if (
2039
2082
  data.collaboratorType !== undefined ||
2040
2083
  data.collaboratorTypeId !== undefined ||
@@ -2127,6 +2170,7 @@ export class OperationsService {
2127
2170
  data.compensationAmount !== undefined ||
2128
2171
  data.contractDescription !== undefined ||
2129
2172
  data.autoGenerateContractDraft !== undefined ||
2173
+ data.hourlyRate !== undefined ||
2130
2174
  data.joinedAt !== undefined ||
2131
2175
  data.weeklyCapacityHours !== undefined ||
2132
2176
  data.supervisorCollaboratorId !== undefined ||
@@ -2137,6 +2181,29 @@ export class OperationsService {
2137
2181
  data.personId !== undefined ||
2138
2182
  data.displayName !== undefined
2139
2183
  ) {
2184
+ let currentBudgetAmount: number | null = null;
2185
+
2186
+ if (
2187
+ data.compensationAmount !== undefined &&
2188
+ data.compensationAmount !== null
2189
+ ) {
2190
+ const hiringContracts = (await (tx as any).$queryRawUnsafe(
2191
+ `SELECT budget_amount AS "budgetAmount"
2192
+ FROM operations_contract
2193
+ WHERE related_collaborator_id = $1
2194
+ AND origin_type = 'employee_hiring'
2195
+ AND deleted_at IS NULL
2196
+ ORDER BY created_at DESC
2197
+ LIMIT 1`,
2198
+ collaboratorId
2199
+ )) as { budgetAmount: string | null }[];
2200
+
2201
+ currentBudgetAmount =
2202
+ hiringContracts[0]?.budgetAmount != null
2203
+ ? Number(hiringContracts[0].budgetAmount)
2204
+ : null;
2205
+ }
2206
+
2140
2207
  await this.syncHiringContractDraft(
2141
2208
  tx as any,
2142
2209
  actor.userId,
@@ -2148,18 +2215,51 @@ export class OperationsService {
2148
2215
  data.compensationAmount !== undefined &&
2149
2216
  data.compensationAmount !== null
2150
2217
  ) {
2151
- await this.insertCollaboratorCompensationHistory(
2152
- tx as any,
2153
- collaboratorId,
2154
- Number(data.compensationAmount),
2155
- actor.userId,
2156
- null
2157
- );
2218
+ const newAmount = Number(data.compensationAmount);
2219
+
2220
+ if (
2221
+ currentBudgetAmount === null ||
2222
+ newAmount !== currentBudgetAmount
2223
+ ) {
2224
+ await this.insertCollaboratorCompensationHistory(
2225
+ tx as any,
2226
+ collaboratorId,
2227
+ newAmount,
2228
+ actor.userId,
2229
+ data.compensationNotes ?? null,
2230
+ data.compensationEffectiveDate ?? null
2231
+ );
2232
+ }
2233
+ }
2234
+
2235
+ if (data.hourlyRate !== undefined && data.hourlyRate !== null) {
2236
+ const newRate = Number(data.hourlyRate);
2237
+ if (currentHourlyRate === null || newRate !== currentHourlyRate) {
2238
+ await this.insertCollaboratorCompensationHistory(
2239
+ tx as any,
2240
+ collaboratorId,
2241
+ newRate,
2242
+ actor.userId,
2243
+ data.compensationNotes ?? null,
2244
+ data.compensationEffectiveDate ?? null,
2245
+ 'hourly_rate'
2246
+ );
2247
+ }
2158
2248
  }
2159
2249
  }
2160
2250
  });
2161
2251
 
2162
- return this.getCollaboratorByIdForUser(userId, collaboratorId);
2252
+ const collaboratorResult = await this.getCollaboratorByIdForUser(userId, collaboratorId);
2253
+
2254
+ await this.integrationApi.publishEvent({
2255
+ eventName: 'operations.collaborator.updated',
2256
+ sourceModule: 'operations',
2257
+ aggregateType: 'collaborator',
2258
+ aggregateId: String(collaboratorId),
2259
+ payload: { id: collaboratorId, displayName: data.displayName, status: data.status },
2260
+ }).catch(() => null);
2261
+
2262
+ return collaboratorResult;
2163
2263
  }
2164
2264
 
2165
2265
  async updateCollaboratorProjectAssignment(
@@ -2267,6 +2367,7 @@ export class OperationsService {
2267
2367
  actorUserId: number | null;
2268
2368
  actorName: string | null;
2269
2369
  notes: string | null;
2370
+ amountType: string;
2270
2371
  createdAt: string;
2271
2372
  }>(
2272
2373
  `SELECT h.id,
@@ -2276,6 +2377,7 @@ export class OperationsService {
2276
2377
  h.actor_user_id AS "actorUserId",
2277
2378
  u.name AS "actorName",
2278
2379
  h.notes,
2380
+ h.amount_type AS "amountType",
2279
2381
  h.created_at AS "createdAt"
2280
2382
  FROM operations_collaborator_compensation_history h
2281
2383
  LEFT JOIN "user" u ON u.id = h.actor_user_id
@@ -3139,14 +3241,20 @@ export class OperationsService {
3139
3241
  ]
3140
3242
  );
3141
3243
 
3142
- return this.getProjectBoardTask(created?.id ?? 0);
3244
+ const task = await this.getProjectBoardTask(created?.id ?? 0);
3245
+
3246
+ await this.integrationApi.publishEvent({
3247
+ eventName: 'operations.task.created',
3248
+ sourceModule: 'operations',
3249
+ aggregateType: 'task',
3250
+ aggregateId: String(created?.id ?? 0),
3251
+ payload: { id: created?.id, projectId, name, status: data.status ?? 'todo', priority: data.priority ?? 'medium' },
3252
+ }).catch(() => null);
3253
+
3254
+ return task;
3143
3255
  }
3144
3256
 
3145
- async updateTask(
3146
- userId: number,
3147
- taskId: number,
3148
- data: Partial<TaskPayload>
3149
- ) {
3257
+ async updateTask(userId: number, taskId: number, data: TaskPayload) {
3150
3258
  const actor = await this.getActorContext(userId);
3151
3259
  if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
3152
3260
  throw new ForbiddenException(
@@ -3237,7 +3345,17 @@ export class OperationsService {
3237
3345
  );
3238
3346
  });
3239
3347
 
3240
- return this.getProjectBoardTask(taskId);
3348
+ const taskResult = await this.getProjectBoardTask(taskId);
3349
+
3350
+ await this.integrationApi.publishEvent({
3351
+ eventName: 'operations.task.updated',
3352
+ sourceModule: 'operations',
3353
+ aggregateType: 'task',
3354
+ aggregateId: String(taskId),
3355
+ payload: { id: taskId, name: data.name, status: data.status },
3356
+ }).catch(() => null);
3357
+
3358
+ return taskResult;
3241
3359
  }
3242
3360
 
3243
3361
  async removeTask(userId: number, taskId: number, permanent = false) {
@@ -3275,6 +3393,14 @@ export class OperationsService {
3275
3393
  );
3276
3394
  });
3277
3395
 
3396
+ await this.integrationApi.publishEvent({
3397
+ eventName: 'operations.task.deleted',
3398
+ sourceModule: 'operations',
3399
+ aggregateType: 'task',
3400
+ aggregateId: String(taskId),
3401
+ payload: { id: taskId, projectId: current.projectId, permanent },
3402
+ }).catch(() => null);
3403
+
3278
3404
  return { success: true };
3279
3405
  }
3280
3406
 
@@ -3493,8 +3619,8 @@ export class OperationsService {
3493
3619
  throw new BadRequestException('Comment content is required.');
3494
3620
  }
3495
3621
 
3496
- const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null }>(
3497
- `SELECT id, actor_collaborator_id AS "actorCollaboratorId"
3622
+ const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null; createdAt: Date }>(
3623
+ `SELECT id, actor_collaborator_id AS "actorCollaboratorId", created_at AS "createdAt"
3498
3624
  FROM operations_task_comment
3499
3625
  WHERE id = $1 AND task_id = $2`,
3500
3626
  [commentId, taskId]
@@ -3509,6 +3635,17 @@ export class OperationsService {
3509
3635
  throw new ForbiddenException('You can only edit your own comments.');
3510
3636
  }
3511
3637
 
3638
+ const editSettings = await this.settingService.getSettingValues(['operations.comment-edit-window']);
3639
+ const editWindowMinutes = Number(editSettings['operations.comment-edit-window'] ?? 5);
3640
+ if (editWindowMinutes > 0) {
3641
+ const diffMinutes = (Date.now() - new Date(row.createdAt).getTime()) / 60000;
3642
+ if (diffMinutes > editWindowMinutes) {
3643
+ throw new ForbiddenException(
3644
+ `Comments can only be edited within ${editWindowMinutes} minute(s) of posting.`
3645
+ );
3646
+ }
3647
+ }
3648
+
3512
3649
  await this.queryRows(
3513
3650
  `UPDATE operations_task_comment
3514
3651
  SET content = $1, updated_at = NOW()
@@ -3539,8 +3676,8 @@ export class OperationsService {
3539
3676
  );
3540
3677
  await this.assertProjectAccess(actor, current.projectId);
3541
3678
 
3542
- const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null }>(
3543
- `SELECT id, actor_collaborator_id AS "actorCollaboratorId"
3679
+ const rows = await this.queryRows<{ id: number; actorCollaboratorId: number | null; createdAt: Date }>(
3680
+ `SELECT id, actor_collaborator_id AS "actorCollaboratorId", created_at AS "createdAt"
3544
3681
  FROM operations_task_comment
3545
3682
  WHERE id = $1 AND task_id = $2`,
3546
3683
  [commentId, taskId]
@@ -3555,6 +3692,17 @@ export class OperationsService {
3555
3692
  throw new ForbiddenException('You can only delete your own comments.');
3556
3693
  }
3557
3694
 
3695
+ const deleteSettings = await this.settingService.getSettingValues(['operations.comment-edit-window']);
3696
+ const deleteWindowMinutes = Number(deleteSettings['operations.comment-edit-window'] ?? 5);
3697
+ if (deleteWindowMinutes > 0) {
3698
+ const diffMinutes = (Date.now() - new Date(row.createdAt).getTime()) / 60000;
3699
+ if (diffMinutes > deleteWindowMinutes) {
3700
+ throw new ForbiddenException(
3701
+ `Comments can only be deleted within ${deleteWindowMinutes} minute(s) of posting.`
3702
+ );
3703
+ }
3704
+ }
3705
+
3558
3706
  await this.queryRows(
3559
3707
  `DELETE FROM operations_task_comment WHERE id = $1`,
3560
3708
  [commentId]
@@ -4062,7 +4210,17 @@ export class OperationsService {
4062
4210
  return projectId;
4063
4211
  });
4064
4212
 
4065
- return this.getProjectById(userId, createdProjectId);
4213
+ const result = await this.getProjectById(userId, createdProjectId);
4214
+
4215
+ await this.integrationApi.publishEvent({
4216
+ eventName: 'operations.project.created',
4217
+ sourceModule: 'operations',
4218
+ aggregateType: 'project',
4219
+ aggregateId: String(createdProjectId),
4220
+ payload: { id: createdProjectId, code: data.code, name: data.name, status: data.status ?? 'planning' },
4221
+ }).catch(() => null);
4222
+
4223
+ return result;
4066
4224
  }
4067
4225
 
4068
4226
  async updateProject(userId: number, projectId: number, data: Partial<ProjectPayload>) {
@@ -4159,7 +4317,17 @@ export class OperationsService {
4159
4317
  }
4160
4318
  });
4161
4319
 
4162
- return this.getProjectById(userId, projectId);
4320
+ const projectResult = await this.getProjectById(userId, projectId);
4321
+
4322
+ await this.integrationApi.publishEvent({
4323
+ eventName: 'operations.project.updated',
4324
+ sourceModule: 'operations',
4325
+ aggregateType: 'project',
4326
+ aggregateId: String(projectId),
4327
+ payload: { id: projectId, name: data.name, status: data.status },
4328
+ }).catch(() => null);
4329
+
4330
+ return projectResult;
4163
4331
  }
4164
4332
 
4165
4333
  async listContracts(
@@ -7853,6 +8021,7 @@ export class OperationsService {
7853
8021
  title: string | null;
7854
8022
  levelLabel: string | null;
7855
8023
  weeklyCapacityHours: number | null;
8024
+ hourlyRate: number | null;
7856
8025
  status: string;
7857
8026
  joinedAt: string | null;
7858
8027
  leftAt: string | null;
@@ -7880,6 +8049,7 @@ export class OperationsService {
7880
8049
  COALESCE(NULLIF(job_title_record.name, ''), NULLIF(c.title, '')) AS "title",
7881
8050
  c.level_label AS "levelLabel",
7882
8051
  c.weekly_capacity_hours AS "weeklyCapacityHours",
8052
+ c.hourly_rate AS "hourlyRate",
7883
8053
  c.status,
7884
8054
  c.joined_at AS "joinedAt",
7885
8055
  c.left_at AS "leftAt",
@@ -10146,7 +10316,9 @@ export class OperationsService {
10146
10316
  collaboratorId: number,
10147
10317
  amount: number,
10148
10318
  actorUserId: number | null,
10149
- notes: string | null
10319
+ notes: string | null,
10320
+ effectiveDate?: string | null,
10321
+ amountType: 'salary' | 'hourly_rate' = 'salary'
10150
10322
  ) {
10151
10323
  await client.$executeRawUnsafe(
10152
10324
  `INSERT INTO operations_collaborator_compensation_history (
@@ -10154,14 +10326,18 @@ export class OperationsService {
10154
10326
  amount,
10155
10327
  actor_user_id,
10156
10328
  notes,
10329
+ effective_date,
10330
+ amount_type,
10157
10331
  created_at
10158
10332
  ) VALUES (
10159
- $1, $2, $3, $4, NOW()
10333
+ $1, $2, $3, $4, $5::date, $6::operations_collaborator_compensation_history_am_f803c4196e_enum, NOW()
10160
10334
  )`,
10161
10335
  collaboratorId,
10162
10336
  amount,
10163
10337
  actorUserId,
10164
- notes ?? null
10338
+ notes ?? null,
10339
+ effectiveDate ?? null,
10340
+ amountType
10165
10341
  );
10166
10342
  }
10167
10343