08 — Form Specifications (Organizations Module)
- F1 — Create tenant (S8 create,
CreateOrganizationDto) - F2 — Edit org profile (S2,
UpdateOrganizationDto) - F3 — Branding (S3)
- F4 — Organization settings (S4,
UpdateOrganizationSettingsDto) - F5 — Register (first admin) — cross-module (Auth)
- F6 — Settings collection upsert (module config) — cross-module
- F7 — Feature flag (standalone catalog)
- Cross-form rules
Field-by-field form specs. Server rules are exact from DTOs (
create-organization.dto.ts,update-organization.dto.ts,update-organization-settings.dto.ts,register.dto.ts,update-setting.dto.ts,update-feature-flag.dto.ts). Rules marked(proposed)are client-side only — the backend imposes no min/max/length on those fields. ServerValidationPipe: whitelist + forbidNonWhitelisted + transform (main.ts:50-57).
F1 — Create tenant (S8 create, CreateOrganizationDto)
| Field | Label | Type/Keyboard | Validation (server) | Client rules (proposed) | Default |
|---|---|---|---|---|---|
| name | Institution name | text | required @IsString (create-organization.dto.ts:59-60) | required, 2–120 chars, trimmed | — |
| slug | Tenant slug | text (mono) | optional @IsString (:63-65) | ^[a-z0-9]+(?:-[a-z0-9]+)*$; live SlugPreview of auto-value | auto from name |
| domain | Domain | text (mono) | optional @IsString (:68-70) | lowercase FQDN; unique platform-wide (409) | — |
| contact.email | Contact email | @IsEmail (:16-17) | valid email | — | |
| contact.phone | Contact phone | phone | @IsString (:20-21) | 7–20 digits/+/spaces | — |
| contact.website | Website | url | @IsString (:24-25) | http(s) URL | — |
| address.* | street/city/state/country/zip | text | @IsString each (:35-54) | free text, 120 max | — |
| timezone | Timezone | sheet picker (searchable) | @IsString (:87-89) | IANA name from picker | UTC (organization.schema.ts:69-70) |
| currency | Currency | sheet picker | @IsString (:92-94) | ISO code from picker | USD (:72-73) |
| subscriptionPlan | Plan | dropdown | @IsEnum(SubscriptionPlan) (:100-102) | enum free/basic/premium/enterprise | free |
| branding | Branding | (link to S3) | @IsObject (:106-107) | deferred | — |
| metadata | Metadata | JSON textarea (proposed) | @IsObject (:110-111) | valid JSON object | — |
Submission: CTA "Create tenant"; on success → tenant detail + slug handoff banner. Duplicate email? N/A (contact email not unique). Slug/domain conflicts → 409 inline.
F2 — Edit org profile (S2, UpdateOrganizationDto)
| Field | Label | Rules | Notes |
|---|---|---|---|
| name | Institution name | @IsString optional (update-organization.dto.ts:63-65) | 2–120 (proposed) |
| slug | Tenant slug | optional string (:68-70) | changing = identity change → confirm dialog; conflict 409 (organizations.service.ts:107-113) |
| domain | Domain | optional string (:73-75) | conflict 409 if owned by other org (:115-122) |
| logoFileId | Logo | optional string (:79-80) | set via upload flow (storage (planned)) |
| contact.* | Contact | optional @IsEmail/@IsString (:85-87) | email validated |
| address.* | Address | optional strings (:89-94) | free text |
| timezone / currency | Localization | optional strings (:97-104) | pickers |
| academicYear.startDate / endDate | Academic year | optional Date (:109-111) | IsDate via implicit transform; start < end (proposed) |
| academicYear.month | Year start month | optional number (:112) | 1–12 (proposed) |
| subscriptionPlan / subscriptionStatus | Subscription | enums (:115-123) | dropdowns; read-only for non-billing (proposed) |
| branding | Branding | @IsObject (:126-128) | see F3 |
| status | Status | @IsEnum(OrganizationStatus) (:130-133) | platform surface only (S8) |
| metadata | Metadata | @IsObject (:137-138) | JSON |
Submission: "Save changes" → PATCH :id; partial update allowed (PATCH semantics, $set dto — organizations.service.ts:124); version increments (organizations.repository.ts:55).
F3 — Branding (S3)
| Field | Label | Rules | Notes |
|---|---|---|---|
| primaryColor | Primary color | hex /^#?[0-9A-Fa-f]{6}$/ (proposed) | server: unvalidated Record<string,unknown> (update-organization.dto.ts:126-128); theme override per 02_Design_Tokens.md §1 |
| secondaryColor | Secondary color | hex (proposed) | same |
| logo | Logo | file upload → id (storage (planned)) | organization.schema.ts:118; preview tile |
| favicon | Favicon | file upload (optional) | :119 |
Submission: whole branding object → PATCH :id. Contrast check ≥4.5:1 vs white for primary as CTA bg (proposed) warning.
F4 — Organization settings (S4, UpdateOrganizationSettingsDto)
| Field | Label | Type | Server rule | Client (proposed) | Default |
|---|---|---|---|---|---|
| attendance.gracePeriod | Grace period (min) | number | @IsObject container only (update-organization-settings.dto.ts:8-12) | 0–300, integer | — |
| attendance.lateThreshold | Late threshold (min) | number | same | 0–300 | — |
| attendance.halfDayThreshold | Half-day threshold (min) | number | same | 0–300; > lateThreshold hint | — |
| attendance.workingDays | Working days | day chips (int 0–6) | number[] | ≥1 selected; values 0–6 | — |
| academic.gradingScale | Grading scale | text | :18-21 | presets + free text | — |
| academic.passingPercentage | Passing % | number | same | 0–100 | — |
| theme.primaryColor / secondaryColor | Theme colors | hex | :26-29 | same as F3 | — |
Submission — CRITICAL full-replace: PATCH :id/settings sends the complete merged object {attendance, academic, theme} — partial bodies wipe sibling groups (organizations.service.ts:134). Dirty tracking per tab; one global save.
F5 — Register (first admin) — cross-module (Auth)
| Field | Label | Type | Server rule (exact) |
|---|---|---|---|
| firstName | First name | text | @IsString (register.dto.ts:6-8) |
| lastName | Last name | text | @IsString (:9-11) |
@IsEmail (:13-15); 409 if used (auth.service.ts:56-58) | |||
| password | Password | password | @IsString + @MinLength(8) (:17-20); Argon2id hashed (auth.service.ts:71) |
| phone | Phone | phone | optional @IsString (:22-25) |
| tenantId | Tenant slug | text (mono) | @IsString required (:27-29); no org-existence check (OQ-2) |
Submission: rate 5/min (auth.controller.ts:31); success returns tokens directly (no redirect); autofill hints name, email, new-password (A §10).
F6 — Settings collection upsert (module config) — cross-module
UpdateSettingDto: key (required), value (required, any), group enum academic/attendance/grading/notification/theme/general (setting.schema.ts:7-14), label, description. PUT /settings upsert (settings.controller.ts:37-41); bulk via PUT /settings/bulk (:43-47). Used by S4 reference tabs (read) — write surface owned by feature modules (cross-module).
F7 — Feature flag (standalone catalog)
UpdateFeatureFlagDto: key (required string), enabled (required boolean — update-feature-flag.dto.ts:9-11), label/description/module (optional). PUT /feature-flags upsert (feature-flags.controller.ts:42-46), bulk :48-52, delete :54-58. Upsert only writes enabled + label (feature-flag.repository.ts:32-43).
Cross-form rules
- Validation timing: on submit + after first error on change; error clears on edit (00-shared/03 AppTextField).
- Submit loading: spinner in CTA, anti-double-submit; success haptic
lightImpact, errormediumImpact(M §3). - Offline: forms render from last-good cache (S2/S4), submit disabled + banner; drafts kept in-memory (no offline queue — 00-shared/07 §10).
- Conflict UX: 409 → inline on the specific field (slug/domain/email), scroll-to-field, focus.
- Server errors: 400 details mapped per field (
http-exception.filter.ts:103-107); unknown fields → "Unexpected field" (forbidNonWhitelisted).