mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
275 lines
9.2 KiB
TypeScript
275 lines
9.2 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Param,
|
|
ParseIntPipe,
|
|
ParseUUIDPipe,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
} from "@nestjs/common";
|
|
import {
|
|
ApiBearerAuth,
|
|
ApiOperation,
|
|
ApiResponse,
|
|
ApiTags,
|
|
} from "@nestjs/swagger";
|
|
import { CurrentUser } from "@tria-plc/api-common/modules/auth/decorators/current-user.decorator";
|
|
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
|
|
|
import { HrStaff } from "../../common/hr-guards";
|
|
import { actorFrom } from "../../common/current-actor.util";
|
|
import { HR_PERMS, IAM_PERMS } from "../../seed/hr-permissions.registry";
|
|
import { EmployeesService } from "./employees.service";
|
|
import { EmployeeDocumentsService } from "./employee-documents.service";
|
|
import { CreateEmployeeProfileDto } from "./dto/create-employee-profile.dto";
|
|
import {
|
|
TerminateEmployeeDto,
|
|
UpdateEmployeeProfileDto,
|
|
} from "./dto/update-employee-profile.dto";
|
|
import { FindEmployeeProfilesDto } from "./dto/find-employee-profiles.dto";
|
|
import { CreateEmployeeDocumentDto } from "./dto/create-employee-document.dto";
|
|
|
|
@ApiTags("employee-profiles")
|
|
@ApiBearerAuth()
|
|
@Controller("employee-profiles")
|
|
// The class gate lists every key its routes use: Nest runs class AND method
|
|
// guards, so a key missing here denies before the route's own key is evaluated.
|
|
@HrStaff([
|
|
HR_PERMS.employeeProfile.view,
|
|
HR_PERMS.employeeProfile.viewOwn,
|
|
HR_PERMS.employeeProfile.create,
|
|
HR_PERMS.employeeProfile.update,
|
|
HR_PERMS.employeeProfile.deactivate,
|
|
HR_PERMS.employeeProfile.terminate,
|
|
IAM_PERMS.employee.activate,
|
|
IAM_PERMS.employee.deactivate,
|
|
HR_PERMS.employeeDocument.view,
|
|
HR_PERMS.employeeDocument.upload,
|
|
HR_PERMS.employeeDocument.delete,
|
|
])
|
|
export class EmployeesController {
|
|
constructor(
|
|
private readonly employeesService: EmployeesService,
|
|
private readonly documentsService: EmployeeDocumentsService,
|
|
) {}
|
|
|
|
@Post()
|
|
@HrStaff(HR_PERMS.employeeProfile.create)
|
|
@ApiOperation({
|
|
summary: "Create the HR profile for an existing IAM employee",
|
|
description:
|
|
"The employee must already exist in iam.employees. Employee number is generated when omitted.",
|
|
})
|
|
@ApiResponse({ status: 409, description: "This employee already has a profile" })
|
|
create(
|
|
@Body() dto: CreateEmployeeProfileDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.create(dto, actorFrom(user));
|
|
}
|
|
|
|
@Get()
|
|
@HrStaff(HR_PERMS.employeeProfile.view)
|
|
@ApiOperation({
|
|
summary: "Employee directory — every IAM employee, with onboarding state",
|
|
description:
|
|
"Driven from iam.employees, left-joined to hr.employee_profiles. " +
|
|
"`isOnboarded` says whether HR holds a profile. Filter with " +
|
|
"?onboarded=false to get the onboarding queue.",
|
|
})
|
|
findAll(
|
|
@Query() filters: FindEmployeeProfilesDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.findDirectory(filters, actorFrom(user));
|
|
}
|
|
|
|
@Post("by-employee/:employeeId/ensure")
|
|
@HrStaff(HR_PERMS.employeeProfile.create)
|
|
@ApiOperation({
|
|
summary: "Onboard an IAM employee — return their HR profile, creating it if absent",
|
|
description:
|
|
"Idempotent. A newly created profile is flagged `isProvisional`: its hire " +
|
|
"date is the IAM record's creation date and its employment type a default, " +
|
|
"so both must be confirmed before payroll or leave accrual rely on them.",
|
|
})
|
|
ensure(
|
|
@Param("employeeId", ParseUUIDPipe) employeeId: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.ensureProfile(employeeId, actorFrom(user));
|
|
}
|
|
|
|
// Declared before `:id` so the literal path is not captured by the UUID route.
|
|
@Get("me")
|
|
@HrStaff(HR_PERMS.employeeProfile.viewOwn)
|
|
@ApiOperation({ summary: "The signed-in employee's own profile" })
|
|
findOwn(@CurrentUser() user: TCurrentUser) {
|
|
return this.employeesService.findOwn(actorFrom(user));
|
|
}
|
|
|
|
@Get("headcount-summary")
|
|
@HrStaff(HR_PERMS.employeeProfile.view)
|
|
@ApiOperation({ summary: "Headcount by employment state, for the dashboard" })
|
|
headcountSummary(@CurrentUser() user: TCurrentUser) {
|
|
return this.employeesService.headcountSummary(actorFrom(user));
|
|
}
|
|
|
|
@Get("documents/expiring")
|
|
@HrStaff(HR_PERMS.employeeDocument.view)
|
|
@ApiOperation({
|
|
summary: "Employee documents expiring within the given number of days",
|
|
})
|
|
expiringDocuments(
|
|
@Query("days", new ParseIntPipe({ optional: true })) days = 30,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.documentsService.expiring(days, actorFrom(user));
|
|
}
|
|
|
|
@Get(":id")
|
|
@HrStaff(HR_PERMS.employeeProfile.view)
|
|
@ApiOperation({ summary: "One employee profile, with IAM facts read through" })
|
|
findOne(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.findOne(id, actorFrom(user));
|
|
}
|
|
|
|
@Patch(":id")
|
|
@HrStaff(HR_PERMS.employeeProfile.update)
|
|
@ApiOperation({
|
|
summary: "Update an employee profile",
|
|
description:
|
|
"Employment state is not editable here — use the suspend / reinstate / terminate transitions.",
|
|
})
|
|
update(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: UpdateEmployeeProfileDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.update(id, dto, actorFrom(user));
|
|
}
|
|
|
|
@Patch(":id/suspend")
|
|
@HrStaff(HR_PERMS.employeeProfile.deactivate)
|
|
@ApiOperation({ summary: "Suspend an employee" })
|
|
suspend(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.suspend(id, actorFrom(user));
|
|
}
|
|
|
|
@Patch(":id/reinstate")
|
|
@HrStaff(HR_PERMS.employeeProfile.deactivate)
|
|
@ApiOperation({ summary: "Return a suspended or on-leave employee to active" })
|
|
reinstate(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.reinstate(id, actorFrom(user));
|
|
}
|
|
|
|
@Patch(":id/terminate")
|
|
@HrStaff(HR_PERMS.employeeProfile.terminate)
|
|
@ApiOperation({
|
|
summary: "End an employment (terminal)",
|
|
description:
|
|
"The row is kept, not deleted — payroll, tax and pension reports read terminated employees for the period they worked.",
|
|
})
|
|
terminate(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: TerminateEmployeeDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.terminate(id, dto, actorFrom(user));
|
|
}
|
|
|
|
// ── Documents ────────────────────────────────────────────────────────────
|
|
|
|
@Patch(":id/system-access/revoke")
|
|
@HrStaff(IAM_PERMS.employee.deactivate)
|
|
@ApiOperation({
|
|
summary: "Deactivate this employee's IAM account",
|
|
description:
|
|
"Revokes the login and ends every position they hold in IAM. Their HR " +
|
|
"employment state is untouched — this is about access, not employment. " +
|
|
"Not reversible in one step: restoring the account does not restore the " +
|
|
"positions.",
|
|
})
|
|
@ApiResponse({ status: 200, description: "Account deactivated" })
|
|
revokeSystemAccess(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.setIamAccountActive(id, false, actorFrom(user));
|
|
}
|
|
|
|
@Patch(":id/system-access/restore")
|
|
@HrStaff(IAM_PERMS.employee.activate)
|
|
@ApiOperation({
|
|
summary: "Reactivate this employee's IAM account",
|
|
description:
|
|
"Restores the login. Position assignments ended by a deactivation are NOT " +
|
|
"restored — reassign them from the organisation explorer. Rejected while " +
|
|
"the employment is TERMINATED or RETIRED.",
|
|
})
|
|
@ApiResponse({ status: 200, description: "Account reactivated" })
|
|
@ApiResponse({
|
|
status: 400,
|
|
description: "The employment has ended; reinstate it first",
|
|
})
|
|
restoreSystemAccess(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.employeesService.setIamAccountActive(id, true, actorFrom(user));
|
|
}
|
|
|
|
@Get(":id/documents")
|
|
@HrStaff(HR_PERMS.employeeDocument.view)
|
|
@ApiOperation({ summary: "Documents filed against an employee" })
|
|
listDocuments(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.documentsService.list(id, actorFrom(user));
|
|
}
|
|
|
|
@Post(":id/documents")
|
|
@HrStaff(HR_PERMS.employeeDocument.upload)
|
|
@ApiOperation({
|
|
summary: "File a document against an employee",
|
|
description: "Upload the file to DMS first, then record its id here.",
|
|
})
|
|
addDocument(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Body() dto: CreateEmployeeDocumentDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.documentsService.add(id, dto, actorFrom(user));
|
|
}
|
|
|
|
@Delete(":id/documents/:documentId")
|
|
@HrStaff(HR_PERMS.employeeDocument.delete)
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
@ApiOperation({
|
|
summary: "Remove a document from an employee's file",
|
|
description: "Soft-deletes the HR index row; the DMS object is untouched.",
|
|
})
|
|
removeDocument(
|
|
@Param("id", ParseUUIDPipe) id: string,
|
|
@Param("documentId", ParseUUIDPipe) documentId: string,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
return this.documentsService.remove(id, documentId, actorFrom(user));
|
|
}
|
|
}
|