Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Pulse360_Runtime_Bug_Fixes_2026-08-08.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading