diff --git a/Pulse360_Runtime_Bug_Fixes_2026-08-08.md b/Pulse360_Runtime_Bug_Fixes_2026-08-08.md index 446fc7e..c2fb5c3 100644 --- a/Pulse360_Runtime_Bug_Fixes_2026-08-08.md +++ b/Pulse360_Runtime_Bug_Fixes_2026-08-08.md @@ -11,6 +11,8 @@ - MVP 2 needs a separate System Admin persona for platform adoption, audit, AI usage, and behavioral metadata monitoring without exposing review scores/comments. - HR report export and self-improvement CSV export attempted to write files to a hard-coded server-side Windows path. - Users needed a self-service way to edit basic profile details. +- MVP 2 analytics needed cleaner event tables instead of storing every downstream metric only in `audit_log`. +- HR needed editable workforce dimensions for job grade, employment type, conversion-hire status, gender, and ethnicity. ## Fixes Applied @@ -31,6 +33,9 @@ - CSV export now creates a browser-side CSV download instead of writing to `C:\Users\...`. - Added `/profile` and `/api/profile` so users can edit first name, last name, email, and job title. - Removed `next/font/google` usage so production builds do not depend on fetching Google Fonts. +- Added dedicated event projection tables for auth, profile updates, AI usage, AI HITL decisions, nominations, and reviews while keeping `audit_log` as the immutable governance feed. +- Added HR-editable employee workforce fields: `employment_type`, `conversion_hire_status`, `gender`, and `ethnicity`. +- Updated the System Admin dashboard to read login and AI token metrics from the dedicated event tables and show aggregated gender/employment context without exposing ethnicity. ## Verification @@ -51,3 +56,5 @@ - Confirm the HR report preview includes score cards and analytics bar charts before approval. - Download a self-improvement plan and confirm the file is a `.csv`. - Visit My Profile and confirm basic details can be saved. +- As HR Admin, create or edit an employee and confirm job grade, employment type, conversion-hire status, gender, and ethnicity persist. +- As System Admin, confirm adoption, AI token, gender, and employment mix cards update without showing employee-level ethnicity. diff --git a/README.md b/README.md index 4029a31..64b1b97 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ Scripts are applied in order on first container start: | `07_seed_employees.sql` | 57 synthetic employees with manager relationships | | `08_add_password_hash.sql` | bcrypt password hashes for all 57 employees | -### Data Model (11 tables) +### Data Model (17 tables) | Table | Purpose | |---|---| @@ -196,7 +196,15 @@ Scripts are applied in order on first container start: | `review` | Submitted reviews per reviewer per subject | | `review_rating` | Individual question answers (score or text) | | `review_result` | Computed aggregates written at CALCULATION phase | -| `audit_log` | Append-only event log | +| `audit_log` | Append-only raw governance event log | +| `auth_event` | Login success/failure projection for adoption and access monitoring | +| `profile_event` | Profile update projection showing which fields changed without exposing old/new values | +| `ai_usage_event` | AI feature usage, status, model, and token metadata | +| `ai_hitl_decision` | Human-in-the-loop accept/edit/discard decisions for AI-generated content | +| `nomination_event` | Nomination lifecycle events with employee/reviewer and department snapshots | +| `review_event` | Review draft/submission metadata without review comments or scores | + +The `employee` table now also carries HR-managed workforce dimensions: job grade, employment type, conversion-hire status, gender, and ethnicity. System Admin analytics read from dedicated event tables and aggregated workforce dimensions. Ethnicity is intentionally kept for HR demographic reporting and is not displayed on the System Admin dashboard. ### Verify the database diff --git a/pulse360/prisma/migrations/000004_employee_demographics_and_event_tables/migration.sql b/pulse360/prisma/migrations/000004_employee_demographics_and_event_tables/migration.sql new file mode 100644 index 0000000..36e78ac --- /dev/null +++ b/pulse360/prisma/migrations/000004_employee_demographics_and_event_tables/migration.sql @@ -0,0 +1,168 @@ +CREATE TYPE "employment_type" AS ENUM ('INTERNSHIP', 'LEARNERSHIP', 'CONTRACT', 'PERMANENT'); +CREATE TYPE "conversion_hire_status" AS ENUM ('NO', 'YES', 'PENDING_DECISION', 'REVIEWED'); +CREATE TYPE "gender" AS ENUM ('WOMAN', 'MAN', 'NON_BINARY', 'OTHER', 'PREFER_NOT_TO_SAY'); +CREATE TYPE "ethnicity" AS ENUM ('BLACK', 'WHITE', 'COLOURED', 'ASIAN', 'INDIAN', 'OTHER', 'PREFER_NOT_TO_SAY'); +CREATE TYPE "auth_event_status" AS ENUM ('SUCCESS', 'FAILURE'); +CREATE TYPE "ai_feature" AS ENUM ('SUGGEST_COMMENTS', 'THEME_SUMMARY', 'IMPROVEMENT_PLAN', 'ANALYTICS_REPORT'); +CREATE TYPE "ai_usage_status" AS ENUM ('SUCCESS', 'ERROR'); +CREATE TYPE "ai_hitl_decision_type" AS ENUM ('ACCEPTED', 'EDITED', 'DISCARDED'); +CREATE TYPE "nomination_event_action" AS ENUM ('CREATED', 'REMOVED', 'SUBMITTED', 'APPROVED', 'REJECTED', 'BULK_APPROVED'); +CREATE TYPE "review_event_action" AS ENUM ('DRAFT_SAVED', 'SUBMITTED'); + +ALTER TABLE "employee" + ADD COLUMN "employment_type" "employment_type" NOT NULL DEFAULT 'PERMANENT', + ADD COLUMN "conversion_hire_status" "conversion_hire_status" NOT NULL DEFAULT 'NO', + ADD COLUMN "gender" "gender", + ADD COLUMN "ethnicity" "ethnicity"; + +CREATE TABLE "auth_event" ( + "id" BIGSERIAL NOT NULL, + "actor_id" INTEGER, + "email" TEXT, + "role" "employee_role", + "department_id" INTEGER, + "department_name" VARCHAR(100), + "status" "auth_event_status" NOT NULL, + "failure_reason" VARCHAR(80), + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "auth_event_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "profile_event" ( + "id" BIGSERIAL NOT NULL, + "actor_id" INTEGER NOT NULL, + "employee_id" INTEGER NOT NULL, + "changed_fields" JSONB NOT NULL, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "profile_event_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "ai_usage_event" ( + "id" BIGSERIAL NOT NULL, + "actor_id" INTEGER, + "feature" "ai_feature" NOT NULL, + "model" VARCHAR(120), + "status" "ai_usage_status" NOT NULL, + "stub" BOOLEAN NOT NULL DEFAULT false, + "input_tokens" INTEGER NOT NULL DEFAULT 0, + "output_tokens" INTEGER NOT NULL DEFAULT 0, + "total_tokens" INTEGER NOT NULL DEFAULT 0, + "cycle_id" INTEGER, + "entity_type" VARCHAR(50), + "entity_id" INTEGER, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ai_usage_event_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "ai_hitl_decision" ( + "id" BIGSERIAL NOT NULL, + "ai_usage_event_id" BIGINT, + "actor_id" INTEGER, + "feature" "ai_feature" NOT NULL, + "decision" "ai_hitl_decision_type" NOT NULL, + "cycle_id" INTEGER, + "entity_type" VARCHAR(50), + "entity_id" INTEGER, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ai_hitl_decision_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "nomination_event" ( + "id" BIGSERIAL NOT NULL, + "actor_id" INTEGER, + "nomination_id" INTEGER, + "cycle_id" INTEGER, + "employee_id" INTEGER, + "reviewer_id" INTEGER, + "action" "nomination_event_action" NOT NULL, + "previous_approval_status" "nomination_approval", + "approval_status" "nomination_approval", + "submission_status" "nomination_submission", + "employee_department_id" INTEGER, + "reviewer_department_id" INTEGER, + "employee_department_name" VARCHAR(100), + "reviewer_department_name" VARCHAR(100), + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "nomination_event_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "review_event" ( + "id" BIGSERIAL NOT NULL, + "actor_id" INTEGER, + "review_id" INTEGER, + "cycle_id" INTEGER, + "employee_id" INTEGER, + "reviewer_id" INTEGER, + "action" "review_event_action" NOT NULL, + "status" "review_status", + "rating_count" INTEGER NOT NULL DEFAULT 0, + "has_do_well_comment" BOOLEAN NOT NULL DEFAULT false, + "has_improve_comment" BOOLEAN NOT NULL DEFAULT false, + "has_attention_comment" BOOLEAN NOT NULL DEFAULT false, + "would_pick_for_team" BOOLEAN, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "review_event_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "auth_event_created_at_idx" ON "auth_event"("created_at"); +CREATE INDEX "auth_event_actor_id_created_at_idx" ON "auth_event"("actor_id", "created_at"); +CREATE INDEX "auth_event_status_created_at_idx" ON "auth_event"("status", "created_at"); + +CREATE INDEX "profile_event_created_at_idx" ON "profile_event"("created_at"); +CREATE INDEX "profile_event_actor_id_created_at_idx" ON "profile_event"("actor_id", "created_at"); +CREATE INDEX "profile_event_employee_id_created_at_idx" ON "profile_event"("employee_id", "created_at"); + +CREATE INDEX "ai_usage_event_created_at_idx" ON "ai_usage_event"("created_at"); +CREATE INDEX "ai_usage_event_actor_id_created_at_idx" ON "ai_usage_event"("actor_id", "created_at"); +CREATE INDEX "ai_usage_event_feature_created_at_idx" ON "ai_usage_event"("feature", "created_at"); +CREATE INDEX "ai_usage_event_cycle_id_created_at_idx" ON "ai_usage_event"("cycle_id", "created_at"); + +CREATE INDEX "ai_hitl_decision_created_at_idx" ON "ai_hitl_decision"("created_at"); +CREATE INDEX "ai_hitl_decision_actor_id_created_at_idx" ON "ai_hitl_decision"("actor_id", "created_at"); +CREATE INDEX "ai_hitl_decision_feature_created_at_idx" ON "ai_hitl_decision"("feature", "created_at"); +CREATE INDEX "ai_hitl_decision_decision_created_at_idx" ON "ai_hitl_decision"("decision", "created_at"); + +CREATE INDEX "nomination_event_created_at_idx" ON "nomination_event"("created_at"); +CREATE INDEX "nomination_event_action_created_at_idx" ON "nomination_event"("action", "created_at"); +CREATE INDEX "nomination_event_cycle_id_created_at_idx" ON "nomination_event"("cycle_id", "created_at"); +CREATE INDEX "nomination_event_employee_id_created_at_idx" ON "nomination_event"("employee_id", "created_at"); +CREATE INDEX "nomination_event_reviewer_id_created_at_idx" ON "nomination_event"("reviewer_id", "created_at"); + +CREATE INDEX "review_event_created_at_idx" ON "review_event"("created_at"); +CREATE INDEX "review_event_action_created_at_idx" ON "review_event"("action", "created_at"); +CREATE INDEX "review_event_cycle_id_created_at_idx" ON "review_event"("cycle_id", "created_at"); +CREATE INDEX "review_event_employee_id_created_at_idx" ON "review_event"("employee_id", "created_at"); +CREATE INDEX "review_event_reviewer_id_created_at_idx" ON "review_event"("reviewer_id", "created_at"); + +ALTER TABLE "auth_event" ADD CONSTRAINT "auth_event_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "auth_event" ADD CONSTRAINT "auth_event_department_id_fkey" FOREIGN KEY ("department_id") REFERENCES "department"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "profile_event" ADD CONSTRAINT "profile_event_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employee"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "profile_event" ADD CONSTRAINT "profile_event_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employee"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE "ai_usage_event" ADD CONSTRAINT "ai_usage_event_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "ai_usage_event" ADD CONSTRAINT "ai_usage_event_cycle_id_fkey" FOREIGN KEY ("cycle_id") REFERENCES "review_cycle"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "ai_hitl_decision" ADD CONSTRAINT "ai_hitl_decision_ai_usage_event_id_fkey" FOREIGN KEY ("ai_usage_event_id") REFERENCES "ai_usage_event"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "ai_hitl_decision" ADD CONSTRAINT "ai_hitl_decision_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "ai_hitl_decision" ADD CONSTRAINT "ai_hitl_decision_cycle_id_fkey" FOREIGN KEY ("cycle_id") REFERENCES "review_cycle"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "nomination_event" ADD CONSTRAINT "nomination_event_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "nomination_event" ADD CONSTRAINT "nomination_event_nomination_id_fkey" FOREIGN KEY ("nomination_id") REFERENCES "nomination"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "nomination_event" ADD CONSTRAINT "nomination_event_cycle_id_fkey" FOREIGN KEY ("cycle_id") REFERENCES "review_cycle"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "nomination_event" ADD CONSTRAINT "nomination_event_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "nomination_event" ADD CONSTRAINT "nomination_event_reviewer_id_fkey" FOREIGN KEY ("reviewer_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "nomination_event" ADD CONSTRAINT "nomination_event_employee_department_id_fkey" FOREIGN KEY ("employee_department_id") REFERENCES "department"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "nomination_event" ADD CONSTRAINT "nomination_event_reviewer_department_id_fkey" FOREIGN KEY ("reviewer_department_id") REFERENCES "department"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "review_event" ADD CONSTRAINT "review_event_actor_id_fkey" FOREIGN KEY ("actor_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "review_event" ADD CONSTRAINT "review_event_review_id_fkey" FOREIGN KEY ("review_id") REFERENCES "review"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "review_event" ADD CONSTRAINT "review_event_cycle_id_fkey" FOREIGN KEY ("cycle_id") REFERENCES "review_cycle"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "review_event" ADD CONSTRAINT "review_event_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "review_event" ADD CONSTRAINT "review_event_reviewer_id_fkey" FOREIGN KEY ("reviewer_id") REFERENCES "employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/pulse360/prisma/schema.prisma b/pulse360/prisma/schema.prisma index ff7f95c..ce84049 100644 --- a/pulse360/prisma/schema.prisma +++ b/pulse360/prisma/schema.prisma @@ -17,6 +17,95 @@ enum EmployeeRole { @@map("employee_role") } +enum EmploymentType { + INTERNSHIP + LEARNERSHIP + CONTRACT + PERMANENT + + @@map("employment_type") +} + +enum ConversionHireStatus { + NO + YES + PENDING_DECISION + REVIEWED + + @@map("conversion_hire_status") +} + +enum Gender { + WOMAN + MAN + NON_BINARY + OTHER + PREFER_NOT_TO_SAY + + @@map("gender") +} + +enum Ethnicity { + BLACK + WHITE + COLOURED + ASIAN + INDIAN + OTHER + PREFER_NOT_TO_SAY + + @@map("ethnicity") +} + +enum AuthEventStatus { + SUCCESS + FAILURE + + @@map("auth_event_status") +} + +enum AiFeature { + SUGGEST_COMMENTS + THEME_SUMMARY + IMPROVEMENT_PLAN + ANALYTICS_REPORT + + @@map("ai_feature") +} + +enum AiUsageStatus { + SUCCESS + ERROR + + @@map("ai_usage_status") +} + +enum AiHitlDecisionType { + ACCEPTED + EDITED + DISCARDED + + @@map("ai_hitl_decision_type") +} + +enum NominationEventAction { + CREATED + REMOVED + SUBMITTED + APPROVED + REJECTED + BULK_APPROVED + + @@map("nomination_event_action") +} + +enum ReviewEventAction { + DRAFT_SAVED + SUBMITTED + + @@map("review_event_action") +} + enum CyclePhase { DRAFT NOMINATE @@ -74,6 +163,9 @@ model Department { name String @unique @db.VarChar(100) description String? employees Employee[] + authEvents AuthEvent[] + nominationEventsAsEmployeeDept NominationEvent[] @relation("NominationEventEmployeeDepartment") + nominationEventsAsReviewerDept NominationEvent[] @relation("NominationEventReviewerDepartment") @@map("department") } @@ -86,6 +178,10 @@ model Employee { email String @unique jobTitle String? @db.VarChar(200) @map("job_title") jobGrade String? @db.VarChar(60) @map("job_grade") + employmentType EmploymentType @default(PERMANENT) @map("employment_type") + conversionHireStatus ConversionHireStatus @default(NO) @map("conversion_hire_status") + gender Gender? + ethnicity Ethnicity? passwordHash String? @map("password_hash") departmentId Int @map("department_id") managerId Int? @map("manager_id") @@ -105,6 +201,17 @@ model Employee { reviewsReceived Review[] @relation("ReviewEmployee") results ReviewResult[] auditActions AuditLog[] + authEvents AuthEvent[] + profileEventsActed ProfileEvent[] @relation("ProfileEventActor") + profileEventsReceived ProfileEvent[] @relation("ProfileEventEmployee") + aiUsageEvents AiUsageEvent[] + aiHitlDecisions AiHitlDecision[] + nominationEventsActed NominationEvent[] @relation("NominationEventActor") + nominationEventsAsEmployee NominationEvent[] @relation("NominationEventEmployee") + nominationEventsAsReviewer NominationEvent[] @relation("NominationEventReviewer") + reviewEventsActed ReviewEvent[] @relation("ReviewEventActor") + reviewEventsAsEmployee ReviewEvent[] @relation("ReviewEventEmployee") + reviewEventsAsReviewer ReviewEvent[] @relation("ReviewEventReviewer") @@map("employee") } @@ -126,6 +233,10 @@ model ReviewCycle { nominations Nomination[] reviews Review[] results ReviewResult[] + aiUsageEvents AiUsageEvent[] + aiHitlDecisions AiHitlDecision[] + nominationEvents NominationEvent[] + reviewEvents ReviewEvent[] @@map("review_cycle") } @@ -186,6 +297,7 @@ model Nomination { cycle ReviewCycle @relation(fields: [cycleId], references: [id], onDelete: Cascade) employee Employee @relation("NominationEmployee", fields: [employeeId], references: [id]) reviewer Employee @relation("NominationReviewer", fields: [reviewerId], references: [id]) + events NominationEvent[] @@unique([cycleId, employeeId, reviewerId]) @@map("nomination") @@ -207,6 +319,7 @@ model Review { reviewer Employee @relation("ReviewReviewer", fields: [reviewerId], references: [id]) employee Employee @relation("ReviewEmployee", fields: [employeeId], references: [id]) ratings ReviewRating[] + events ReviewEvent[] @@unique([cycleId, reviewerId, employeeId]) @@map("review") @@ -259,3 +372,156 @@ model AuditLog { @@map("audit_log") } + +model AuthEvent { + id BigInt @id @default(autoincrement()) + actorId Int? @map("actor_id") + email String? + role EmployeeRole? + departmentId Int? @map("department_id") + departmentName String? @db.VarChar(100) @map("department_name") + status AuthEventStatus + failureReason String? @db.VarChar(80) @map("failure_reason") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + actor Employee? @relation(fields: [actorId], references: [id], onDelete: SetNull) + department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull) + + @@index([createdAt]) + @@index([actorId, createdAt]) + @@index([status, createdAt]) + @@map("auth_event") +} + +model ProfileEvent { + id BigInt @id @default(autoincrement()) + actorId Int @map("actor_id") + employeeId Int @map("employee_id") + changedFields Json @map("changed_fields") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + actor Employee @relation("ProfileEventActor", fields: [actorId], references: [id]) + employee Employee @relation("ProfileEventEmployee", fields: [employeeId], references: [id]) + + @@index([createdAt]) + @@index([actorId, createdAt]) + @@index([employeeId, createdAt]) + @@map("profile_event") +} + +model AiUsageEvent { + id BigInt @id @default(autoincrement()) + actorId Int? @map("actor_id") + feature AiFeature + model String? @db.VarChar(120) + status AiUsageStatus + stub Boolean @default(false) + inputTokens Int @default(0) @map("input_tokens") + outputTokens Int @default(0) @map("output_tokens") + totalTokens Int @default(0) @map("total_tokens") + cycleId Int? @map("cycle_id") + entityType String? @db.VarChar(50) @map("entity_type") + entityId Int? @map("entity_id") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + actor Employee? @relation(fields: [actorId], references: [id], onDelete: SetNull) + cycle ReviewCycle? @relation(fields: [cycleId], references: [id], onDelete: SetNull) + decisions AiHitlDecision[] + + @@index([createdAt]) + @@index([actorId, createdAt]) + @@index([feature, createdAt]) + @@index([cycleId, createdAt]) + @@map("ai_usage_event") +} + +model AiHitlDecision { + id BigInt @id @default(autoincrement()) + aiUsageEventId BigInt? @map("ai_usage_event_id") + actorId Int? @map("actor_id") + feature AiFeature + decision AiHitlDecisionType + cycleId Int? @map("cycle_id") + entityType String? @db.VarChar(50) @map("entity_type") + entityId Int? @map("entity_id") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + aiUsageEvent AiUsageEvent? @relation(fields: [aiUsageEventId], references: [id], onDelete: SetNull) + actor Employee? @relation(fields: [actorId], references: [id], onDelete: SetNull) + cycle ReviewCycle? @relation(fields: [cycleId], references: [id], onDelete: SetNull) + + @@index([createdAt]) + @@index([actorId, createdAt]) + @@index([feature, createdAt]) + @@index([decision, createdAt]) + @@map("ai_hitl_decision") +} + +model NominationEvent { + id BigInt @id @default(autoincrement()) + actorId Int? @map("actor_id") + nominationId Int? @map("nomination_id") + cycleId Int? @map("cycle_id") + employeeId Int? @map("employee_id") + reviewerId Int? @map("reviewer_id") + action NominationEventAction + previousApprovalStatus NominationApproval? @map("previous_approval_status") + approvalStatus NominationApproval? @map("approval_status") + submissionStatus NominationSubmission? @map("submission_status") + employeeDepartmentId Int? @map("employee_department_id") + reviewerDepartmentId Int? @map("reviewer_department_id") + employeeDepartmentName String? @db.VarChar(100) @map("employee_department_name") + reviewerDepartmentName String? @db.VarChar(100) @map("reviewer_department_name") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + actor Employee? @relation("NominationEventActor", fields: [actorId], references: [id], onDelete: SetNull) + nomination Nomination? @relation(fields: [nominationId], references: [id], onDelete: SetNull) + cycle ReviewCycle? @relation(fields: [cycleId], references: [id], onDelete: SetNull) + employee Employee? @relation("NominationEventEmployee", fields: [employeeId], references: [id], onDelete: SetNull) + reviewer Employee? @relation("NominationEventReviewer", fields: [reviewerId], references: [id], onDelete: SetNull) + employeeDepartment Department? @relation("NominationEventEmployeeDepartment", fields: [employeeDepartmentId], references: [id], onDelete: SetNull) + reviewerDepartment Department? @relation("NominationEventReviewerDepartment", fields: [reviewerDepartmentId], references: [id], onDelete: SetNull) + + @@index([createdAt]) + @@index([action, createdAt]) + @@index([cycleId, createdAt]) + @@index([employeeId, createdAt]) + @@index([reviewerId, createdAt]) + @@map("nomination_event") +} + +model ReviewEvent { + id BigInt @id @default(autoincrement()) + actorId Int? @map("actor_id") + reviewId Int? @map("review_id") + cycleId Int? @map("cycle_id") + employeeId Int? @map("employee_id") + reviewerId Int? @map("reviewer_id") + action ReviewEventAction + status ReviewStatus? + ratingCount Int @default(0) @map("rating_count") + hasDoWellComment Boolean @default(false) @map("has_do_well_comment") + hasImproveComment Boolean @default(false) @map("has_improve_comment") + hasAttentionComment Boolean @default(false) @map("has_attention_comment") + wouldPickForTeam Boolean? @map("would_pick_for_team") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + + actor Employee? @relation("ReviewEventActor", fields: [actorId], references: [id], onDelete: SetNull) + review Review? @relation(fields: [reviewId], references: [id], onDelete: SetNull) + cycle ReviewCycle? @relation(fields: [cycleId], references: [id], onDelete: SetNull) + employee Employee? @relation("ReviewEventEmployee", fields: [employeeId], references: [id], onDelete: SetNull) + reviewer Employee? @relation("ReviewEventReviewer", fields: [reviewerId], references: [id], onDelete: SetNull) + + @@index([createdAt]) + @@index([action, createdAt]) + @@index([cycleId, createdAt]) + @@index([employeeId, createdAt]) + @@index([reviewerId, createdAt]) + @@map("review_event") +} diff --git a/pulse360/src/app/(app)/employees/[id]/page.tsx b/pulse360/src/app/(app)/employees/[id]/page.tsx index 41689e9..789f968 100644 --- a/pulse360/src/app/(app)/employees/[id]/page.tsx +++ b/pulse360/src/app/(app)/employees/[id]/page.tsx @@ -13,6 +13,11 @@ export default function EmployeeFormPage() { const [lastName, setLastName] = useState(""); const [email, setEmail] = useState(""); const [jobTitle, setJobTitle] = useState(""); + const [jobGrade, setJobGrade] = useState(""); + const [employmentType, setEmploymentType] = useState("PERMANENT"); + const [conversionHireStatus, setConversionHireStatus] = useState("NO"); + const [gender, setGender] = useState(""); + const [ethnicity, setEthnicity] = useState(""); const [role, setRole] = useState("EMPLOYEE"); const [departmentId, setDepartmentId] = useState(""); const [managerId, setManagerId] = useState(""); @@ -38,6 +43,11 @@ export default function EmployeeFormPage() { const emp = await eRes.json(); setFirstName(emp.firstName ?? ""); setLastName(emp.lastName ?? ""); setEmail(emp.email ?? ""); setJobTitle(emp.jobTitle ?? ""); + setJobGrade(emp.jobGrade ?? ""); + setEmploymentType(emp.employmentType ?? "PERMANENT"); + setConversionHireStatus(emp.conversionHireStatus ?? "NO"); + setGender(emp.gender ?? ""); + setEthnicity(emp.ethnicity ?? ""); setRole(emp.role ?? "EMPLOYEE"); setDepartmentId(String(emp.departmentId ?? "")); setManagerId(String(emp.managerId ?? "")); setIsActive(emp.isActive ?? true); } @@ -51,7 +61,16 @@ export default function EmployeeFormPage() { e.preventDefault(); setError(""); setSaving(true); const body = { - firstName, lastName, email, jobTitle, role, + firstName, + lastName, + email, + jobTitle, + jobGrade, + employmentType, + conversionHireStatus, + gender: gender || null, + ethnicity: ethnicity || null, + role, departmentId: Number(departmentId), managerId: managerId ? Number(managerId) : null, isActive, @@ -66,8 +85,10 @@ export default function EmployeeFormPage() { if (loading) return
{isEdit ? "Update employee details" : "Create a new employee account"}
@@ -79,29 +100,37 @@ export default function EmployeeFormPage() {