08 — Form Specifications (Staff Module)
- F1 — Create Staff (
/staff/new) →POST /staff - F2 — Edit Staff (
/staff/:id/edit) →PATCH /staff/:id - F3 — Create Department (
/departments/new) →POST /departments - F4 — Edit Department (
/departments/:id/edit) →PATCH /departments/:id - F5 — Create Designation (
/designations/new) →POST /designations - F6 — Edit Designation (
/designations/:id/edit) →PATCH /designations/:id - Cross-form rules
Field-by-field specs for every create/update form. DTO columns cite the exact validator source. Backend note: all three modules validate via
class-validator; failures return 400VALIDATION_ERRORwitherror.details[](http-exception.filter.ts:28,103-107), mapped to field errors by the client (00-shared/06 §5).
F1 — Create Staff (/staff/new) → POST /staff
Source: create-staff.dto.ts:4-38; staff.controller.ts:24-25.
| # | Field | Control | Required | Validation (source) | Server behavior |
|---|---|---|---|---|---|
| 1 | userId | User picker (sheet, searchable) | Yes | @IsMongoId() (create-staff.dto.ts:5-7) | Stored as ref to User (staff.schema.ts:23-24) |
| 2 | employeeNumber | AppTextField, mono, maxLength 64 (proposed) | Yes | @IsString() (create-staff.dto.ts:9-11) | Trimmed (staff.schema.ts:26-27); unique per tenant — 409 on dup (staff.service.ts:31-36, staff.schema.ts:57) |
| 3 | departmentId | CatalogPickerSheet (departments) | No | @IsOptional() @IsMongoId() (create-staff.dto.ts:13-16) | Ref to Department (staff.schema.ts:29-30) |
| 4 | designationId | CatalogPickerSheet (designations) | No | @IsOptional() @IsMongoId() (create-staff.dto.ts:18-21) | Ref to Designation (staff.schema.ts:32-33) |
| 5 | employmentType | AppDropdown: full_time, part_time, contract, intern | No | @IsOptional() @IsString() (create-staff.dto.ts:23-28) — no @IsEnum; enum is documentation only (staff.schema.ts:14-19) | Defaults to full_time when absent (staff.service.ts:40-41) |
| 6 | joiningDate | AppDatePicker | No | @IsOptional() @IsDateString() (create-staff.dto.ts:30-33) | Converted to Date (staff.service.ts:42) |
| 7 | salaryGrade | AppTextField (free text) | No | @IsOptional() @IsString() (create-staff.dto.ts:35-38) | Trimmed; reference only, not payroll (staff.schema.ts:45-46, Staff.md:60) |
- Not in this form:
status(alwaysactiveon create —staff.service.ts:39) andmetadata(update-only,update-staff.dto.ts:40-42). - Helper texts: "Created as active"; "Defaults to full-time".
- Submit: button
loading:while pending, anti-double-submit (00-shared/08 §6); success → snackbar + navigate to detail (SS2).
F2 — Edit Staff (/staff/:id/edit) → PATCH /staff/:id
Source: update-staff.dto.ts:4-42; all fields optional; prefilled from GET /staff/:id.
Send only changed fields (staff.service.ts:78-91 — changes = audit delta, line 88).
| # | Field | Control | Required | Validation (source) | Notes |
|---|---|---|---|---|---|
| 1 | employeeNumber | AppTextField | No | @IsOptional() @IsString() (update-staff.dto.ts:5-8) | 409 on duplicate (staff.service.ts:31-36 shared check only on create — rename dup-check caveat OQ-9) |
| 2 | departmentId | CatalogPickerSheet | No | @IsOptional() @IsMongoId() (lines 10-13) | Clearing sends null? — see OQ-11 |
| 3 | designationId | CatalogPickerSheet | No | @IsOptional() @IsMongoId() (lines 15-18) | same |
| 4 | employmentType | AppDropdown (4 values) | No | @IsOptional() @IsString() (lines 20-23) | no enum validation |
| 5 | joiningDate | AppDatePicker | No | @IsOptional() @IsDateString() (lines 25-28) | |
| 6 | salaryGrade | AppTextField | No | @IsOptional() @IsString() (lines 30-33) | |
| 7 | status | AppDropdown: active, inactive, on_leave, terminated | No | @IsOptional() @IsString() (lines 35-38) | lifecycle enum staff.schema.ts:7-12; the sanctioned way to change lifecycle state |
| 8 | metadata | MetadataEditor (key/value) | No | @IsOptional() (lines 40-42) | free-form object (staff.schema.ts:51-52) |
F3 — Create Department (/departments/new) → POST /departments
Source: department.dto.ts:4-18; department.controller.ts:26-27.
| # | Field | Control | Required | Validation (source) | Server behavior |
|---|---|---|---|---|---|
| 1 | name | AppTextField | Yes | @IsString() (department.dto.ts:5-7) | Trimmed (department.schema.ts:9-10); unique per tenant — 409 on dup (department.service.ts:23-25, department.schema.ts:24) |
| 2 | code | AppTextField (short, uppercase hint) | No | @IsOptional() @IsString() (department.dto.ts:9-12) | Trimmed (department.schema.ts:12-13) |
| 3 | headId | HeadPicker (active staff) | No | @IsOptional() @IsMongoId() (department.dto.ts:14-17) | Ref to Staff (department.schema.ts:15-16) |
- Server sets
status: 'active'on create (department.service.ts:26-29). - Success → snackbar + navigate to detail (SS7); 409 inline on
name.
F4 — Edit Department (/departments/:id/edit) → PATCH /departments/:id
Source: department.dto.ts:20-39; all optional.
| # | Field | Control | Validation (source) | Notes |
|---|---|---|---|---|
| 1 | name | AppTextField | @IsOptional() @IsString() (lines 22-24) | 409 on duplicate name (department.service.ts:23-25 — check runs on create; rename guard applies, verify OQ-9) |
| 2 | code | AppTextField | @IsOptional() @IsString() (lines 26-29) | |
| 3 | headId | HeadPicker | @IsOptional() @IsMongoId() (lines 31-34) | unset → clear |
| 4 | status | AppDropdown (active/other) | @IsOptional() @IsString() (lines 36-39) | catalog soft-state; no enum (department.schema.ts:18-19 is a plain string) |
F5 — Create Designation (/designations/new) → POST /designations
Source: designation.dto.ts:4-18; designation.controller.ts:26-27.
| # | Field | Control | Required | Validation (source) | Server behavior |
|---|---|---|---|---|---|
| 1 | departmentId | CatalogPickerSheet (departments) | No | @IsOptional() @IsMongoId() (designation.dto.ts:5-8) | Ref to Department (designation.schema.ts:9-10) — reference only, not enforced (OQ-3) |
| 2 | name | AppTextField | Yes | @IsString() (designation.dto.ts:10-12) | Trimmed (designation.schema.ts:12-13); unique per tenant — 409 (designation.service.ts:25-28, designation.schema.ts:24) |
| 3 | level | AppTextField (number) or AppStepper | No | @IsOptional() @IsInt() (designation.dto.ts:14-17) | Defaults to 0 (designation.schema.ts:15-16); non-int → 400 details |
- Server sets
status: 'active'on create (designation.service.ts:29-32).
F6 — Edit Designation (/designations/:id/edit) → PATCH /designations/:id
Source: designation.dto.ts:20-39; all optional: departmentId (22-24), name
(26-29), level (@IsInt(), 31-34), status (36-39).
Cross-form rules
- Errors: first invalid field receives focus on submit; errors clear on edit;
error announced via
Semantics(00-shared/09 §10, 00-shared/03 AppTextField). - 409
DUPLICATE_RESOURCE→ inline conflict message + suggest search/refresh (00-shared/06 §5). - 404 on save (concurrent deletion) → not-found state, do not resubmit.
- 429 → "Too many requests — retry in Ns" + backoff, no auto-retry (00-shared/07 §4).
- Autofill hints on
employeeNumber(autofillHints: 'organization-identifier'(proposed));userIdpicker searchable. - Haptics: success
lightImpact, errormediumImpact(00-shared/08 §3).