This commit is contained in:
2026-07-16 09:53:14 +07:00
parent 0fc112a2e8
commit 29cec708a3
1236 changed files with 212848 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
# AI Development Guide
This document is the project operating guide for AI coding agents. It records the current implementation patterns that must be reused before adding new code.
## Priority Order
When instructions conflict, follow this order:
1. Existing project implementation
2. `docs/AI_DEVELOPMENT_GUIDE.md`
3. `docs/PROJECT_ARCHITECTURE.md`
4. `AGENTS.md`
5. `kiranism-shadcn-dashboard`
6. shadcn/ui
7. TanStack Table, TanStack Query, TanStack Form, and nuqs docs
8. Next.js best practices
## Canonical Features
Use these features as references before creating or changing a feature:
1. `training-records`: canonical full feature for DB-backed CRUD, tables, forms, upload, review flow, React Query, and Route Handlers.
2. `employee-directory`: canonical responsive DataTable and table toolbar customization.
3. `announcements`: canonical lightweight content feature with publish/archive state and file attachment.
4. `audit-logs`: canonical read-only table with filters.
## Feature Structure
Runtime features should live under `src/features/<feature-name>/`.
Preferred structure:
```text
src/features/<feature>/
api/
types.ts
service.ts
queries.ts
mutations.ts
components/
constants/
schemas/
server/
```
Use only the folders a feature actually needs. Do not create placeholder folders.
Dashboard routes live in `src/app/dashboard/<route>/page.tsx`.
Route handlers live in `src/app/api/<feature>/route.ts` and `src/app/api/<feature>/[id]/route.ts`.
## Naming Rules
- Use kebab-case filenames for feature components, routes, and docs.
- Use PascalCase React components.
- Keep API contracts in `api/types.ts`.
- Name table files after local convention: either `<feature>-table.tsx` and `<feature>-columns.tsx`, or `components/<feature>-tables/index.tsx` and `columns.tsx` when the feature already uses that folder.
- Name action menu components consistently, for example `pending-review-action-menu.tsx` or `employee-directory-action-menu.tsx`.
## Data Flow
Use the established app-owned flow:
```text
Page Server Component
-> require*DashboardAccess()
-> searchParamsCache.parse()
-> Listing Component
-> getQueryClient().fetchQuery()
-> HydrationBoundary
-> Client Table/Form Component
-> useSuspenseQuery()/useQuery()
-> api/service.ts
-> local Route Handler
-> server/* data helper
-> Drizzle
```
Do not import Drizzle into UI components.
Do not import mock API data into runtime UI.
## Authentication And Authorization
Use Auth.js and the shared helpers:
- `requireEmployeeDashboardAccess()`
- `requireHRDDashboardAccess()`
- `requireITDashboardAccess()`
- `requireOrganizationAccess()`
- `requireHRD()`
- `isHRD()`, `isIT()`, `getBusinessRole()`
Nav filtering is only UX. Route handlers and server pages must enforce access.
## User And Employee Boundary
Use these definitions consistently during the current migration state:
- `users` = system identity, authentication, session, permissions, and audit actor
- `employees` = HR master data, employee profile, and training ownership
Current runtime reality is transitional:
- `users.employeeId` still exists as a compatibility seam
- `user_employee_maps` still exists as an explicit linking table
- training ownership and reporting scope still resolve primarily through `employees`
Treat the canonical relationship as `User 0..1 <-> 0..1 Employee` until a later migration removes the seam.
Rules:
- actor fields such as `createdBy`, `approvedBy`, `publishedBy`, and audit actor must point to `users`
- owner fields for training targets, compliance, and matrix applicability must point to `employees`
- do not introduce a third user-employee linking mechanism
## Table Pattern
Default table implementation must reuse:
- `DataTable`
- `DataTableToolbar`
- `DataTablePagination`
- `DataTableViewOptions`
- `DataTableColumnHeader`
- `useDataTable`
- TanStack `ColumnDef`
- action menu components for row commands
Canonical references:
- `src/features/training-records/components/training-record-tables/index.tsx`
- `src/features/training-records/components/pending-review-table.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
Rules:
- Do not build manual table pagination for new data tables.
- Do not create a new toolbar if `DataTableToolbar` or a small wrapper around it is enough.
- Use `columnPinning: { right: ['actions'] }` for row actions.
- Use `DataTableColumnHeader` for sortable headers.
- Use `meta` on columns for labels, placeholders, filter variants, options, and class names.
- Put horizontal overflow inside the DataTable shell, not the full page.
Manual tables are acceptable only for tiny static layouts or legacy code being intentionally left unchanged.
## Form Pattern
The project uses TanStack Form, not React Hook Form.
Use:
- `useAppForm`
- `useFormFields`
- field wrappers from `@/components/ui/tanstack-form`
- Zod schemas from `src/features/<feature>/schemas`
- `scrollToFirstError()` where helpful
Canonical references:
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/courses/components/course-form.tsx`
- `src/features/training-policy/components/training-policy-form.tsx`
## Mutation Pattern
Use TanStack Query mutation option factories in `api/mutations.ts`.
```text
component -> useMutation({ ...featureMutation })
mutation -> api/service.ts
service -> apiClient()
route handler -> Drizzle
```
This repo does not use `next-safe-action` as a canonical runtime pattern.
## Persistence Pattern
The project uses PostgreSQL with Drizzle ORM.
Use:
- `src/db/schema.ts`
- `src/lib/db.ts`
- feature `server/*-data.ts` helpers
- route handlers under `src/app/api`
Do not introduce Prisma.
## Dialog And Action Pattern
Use existing shadcn/Radix wrappers:
- `Dialog`
- `Sheet`
- `AlertDialog`
- `AlertModal`
- dropdown action menus via `DropdownMenu`
Row actions should be grouped into action menu components when there are multiple commands.
## Upload Pattern
Reuse existing upload utilities and storage helpers:
- `FileUploader`
- `certificate-storage.ts`
- `announcement-storage.ts`
- `online-lesson-storage.ts`
Do not create new storage logic until the existing helper cannot support the feature.
## Layout Pattern
Dashboard pages must use `PageContainer`.
Rules:
- Do not create a new dashboard shell.
- Do not nest cards inside cards.
- Use cards for forms, filters, empty states, and repeated content items.
- Use `w-full min-w-0 max-w-full` on table containers.
- Keep overflow scoped to tables with `overflow-x-auto`.
- Buttons must not clip on mobile; use wrapping containers and `shrink-0 whitespace-nowrap` when needed.
## UI Rules
- Use `@/components/icons` only for icons.
- Use `cn()` for className composition.
- Use shadcn/ui primitives from `@/components/ui`.
- Use `Badge` for status.
- Use `Button` variants rather than custom button styling.
- Prefer Thai labels in user-facing training-system UI.
## Refactoring Rules
- First inspect existing feature, shared components, hooks, forms, tables, and dialogs.
- If code is off-pattern, propose or perform a scoped refactor before expanding it.
- Do not propagate legacy patterns into new work.
- Avoid broad rewrites unless the requested change touches shared contracts or UI behavior.
- Dashboard `page.tsx` entrypoints must call a shared `require*DashboardAccess()` helper unless the route is an explicit redirect shell or documented exception.
## Required Checklist
Before final response, verify:
- Existing pattern was inspected.
- Shared components/hooks were reused.
- DataTable was used for data tables.
- TanStack Form and Zod were used for forms.
- Route handlers and Drizzle were used for persistence.
- Server-side access checks remain in place.
- Page layout is responsive and does not overflow.
- Icons come from `@/components/icons`.
- No mock data was imported into runtime UI.
Final principle: consistency is more important than novelty. New work should look like it has always belonged to this codebase.

View File

@@ -0,0 +1,229 @@
# Project Architecture
This project is a Next.js 16 App Router training-system dashboard using Auth.js, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind CSS v4, TanStack Query, TanStack Table, TanStack Form, and nuqs.
## Top-Level Structure
```text
src/
app/
api/
auth/
dashboard/
components/
layout/
ui/
forms/
config/
db/
features/
hooks/
lib/
styles/
types/
```
## App Router
Dashboard pages live under `src/app/dashboard`.
Common page pattern:
```text
page.tsx
-> require*DashboardAccess()
-> parse search params
-> PageContainer
-> feature listing/detail/form component
```
API route handlers live under `src/app/api`. Route handlers are the app boundary for persistence and access enforcement.
## Feature Modules
Feature modules live under `src/features/<name>`.
Strong feature examples:
- `training-records`: complete DB-backed feature with tables, forms, upload, review, and certificates.
- `courses`: DB-backed CRUD with table and form.
- `users`: app-owned user management.
- `employee-directory`: responsive directory table and detail/edit flows.
- `announcements`: content publishing with role-aware visibility.
- `online-lessons`: DB-backed lessons with upload, role-aware list/detail/manage flows.
- `audit-logs`: read-only audit table.
Feature dependency direction:
```text
feature components
-> feature api queries/mutations
-> feature api service
-> apiClient
-> route handlers
-> feature server data helpers
-> db/schema and db client
```
Feature components may import shared UI, hooks, utilities, and feature contracts. They must not import Drizzle or database schema directly.
## Shared Modules
Shared UI:
- `src/components/ui/*`: shadcn/Radix primitives and app wrappers.
- `src/components/ui/table/*`: canonical DataTable stack.
- `src/components/ui/tanstack-form.tsx`: canonical form stack.
- `src/components/layout/page-container.tsx`: dashboard page shell.
- `src/components/icons.tsx`: icon registry.
Shared hooks:
- `use-data-table`
- `use-debounce`
- `use-debounced-callback`
- `use-nav`
- `use-mobile`
- `use-media-query`
Shared lib:
- `api-client.ts`
- `db.ts`
- `query-client.ts`
- `searchparams.ts`
- `parsers.ts`
- `format.ts`
- `auth/session.ts`
- `auth/roles.ts`
- storage helpers for certificates, announcements, and online lessons
## Data Model
Core runtime identity is `users`, not legacy `employees`.
During the current remediation phase, the implementation is intentionally split across two related domains:
- `users`: authentication, session, permissions, organization context, and audit actor
- `employees`: HR-owned worker profile, training owner, matrix targeting, and compliance/reporting subject
Current compatibility seams:
- `users.employeeId`
- `user_employee_maps`
Current canonical relationship:
- `User 0..1 <-> 0..1 Employee`
This means some authenticated users may not have employee profiles, and some employees may exist before a user account is provisioned.
Important tables and domains:
- `users`
- `organizations`
- `memberships`
- `courses`
- `training_records`
- `training_matrix`
- `reports`
- `announcements`
- `online_lessons`
- audit logs
`employees` is legacy migration data and should not be used for new runtime behavior.
## Auth And RBAC
Authentication is Auth.js Credentials.
Access helpers:
- `requireSession()`
- `requireOrganizationAccess()`
- `requireEmployeeDashboardAccess()`
- `requireHRDDashboardAccess()`
- `requireITDashboardAccess()`
Business roles:
- `IT`
- `HRD`
- `EMPLOYEE`
System role:
- `super_admin`
Access must be enforced server-side in pages and route handlers. Sidebar filtering is user experience only.
## Data Fetching
Preferred list flow:
```text
server listing component
-> getQueryClient()
-> fetchQuery(queryOptions(filters))
-> HydrationBoundary
-> client component
-> useSuspenseQuery(queryOptions(filters))
```
Query options live in `src/features/<feature>/api/queries.ts`.
Mutations live in `src/features/<feature>/api/mutations.ts`.
Service functions call local route handlers through `apiClient`.
## Tables
Canonical stack:
- TanStack Table
- `useDataTable`
- `DataTable`
- `DataTableToolbar`
- `DataTablePagination`
- `DataTableViewOptions`
- `DataTableColumnHeader`
Tables should manage URL state through nuqs via `useDataTable` and parser helpers.
## Forms
Canonical stack:
- TanStack Form
- Zod
- `useAppForm`
- project field wrappers
Form submit should call feature mutation options. Forms should not call route handlers or Drizzle directly.
## Uploads
Upload flows are feature-specific but should reuse existing helpers:
- certificates: training records
- announcements: announcement attachments
- online lessons: video, attachment, thumbnail
Route handlers validate file type and size before storage.
## Navigation
Navigation lives in `src/config/nav-config.ts`.
Role-aware display is handled through nav access metadata and `use-nav`. This does not replace server-side guards.
## Current Legacy Areas
Some template or older areas remain:
- `products`
- `employees`
- `forms`
- `kanban`
- `chat`
- demo/react-query pages
Treat these as legacy/template features unless the user specifically asks to modernize them.

164
docs/REFRACTOR_REPORT.md Normal file
View File

@@ -0,0 +1,164 @@
# Refactor Report
Generated from `plans/rule-ai.md`.
## Canonical Reference
Primary canonical feature: `training-records`
Why:
- Uses Route Handlers plus Drizzle.
- Has feature API contracts, services, queries, and mutations.
- Uses server prefetch and TanStack Query hydration.
- Uses DataTable, DataTableToolbar, DataTablePagination, and row action menus.
- Uses TanStack Form plus Zod.
- Includes upload and review flows.
- Enforces role-aware behavior.
Secondary references:
- `employee-directory` for responsive table tuning and custom toolbar behavior.
- `announcements` for content publishing and lightweight state transitions.
- `audit-logs` for read-only table pattern.
## Findings
### Announcements
Status: partially off-pattern.
Findings:
- Uses manual `Pagination` rather than `DataTablePagination`.
- Uses card list layout, which may be acceptable for content feeds but should not be copied for data tables.
Recommended refactor:
- Keep card layout if announcements are intentionally content-first.
- If a management table is needed, add a DataTable-based management view instead of extending manual pagination.
### Notifications
Status: partially off-pattern.
Findings:
- Uses manual pagination in `notifications-page.tsx`.
- Should be reviewed if notifications become a true data-management table.
Recommended refactor:
- Convert to DataTable if filtering, sorting, view options, or admin review workflows grow.
### Online Lessons
Status: actively migrating.
Findings:
- Has a full feature module with API, service, queries, mutations, schema, server helper, form, list/detail pages.
- Recent table work should stay aligned to `DataTable` and `useDataTable`.
- Manage/list routes should avoid duplicated table implementations.
Recommended refactor:
- Keep only one canonical list table implementation.
- Remove or redirect legacy manage listing code after confirming no route depends on it.
### Employee Directory
Status: mostly aligned.
Findings:
- Uses DataTable and custom toolbar.
- Good reference for responsive table widths and hidden columns.
- No schema folder because it is currently backed by `users` and server data helpers rather than a new entity schema.
Recommended refactor:
- Keep as secondary canonical table reference.
### Products, Employees, Forms, Kanban, Chat, React Query Demo
Status: legacy/template/demo areas.
Findings:
- Some files still reflect starter-template patterns.
- Products and employees include older table/form conventions.
- Demo pages should not be copied for runtime features.
Recommended refactor:
- Do not use these as references for new features.
- Migrate only when a user-facing requirement touches the feature.
### Reports
Status: feature-specific dashboard view.
Findings:
- Uses custom report page and chart/table cards.
- Lint currently reports unused imports and nested component warnings in reports.
Recommended refactor:
- Clean unused report imports.
- Extract nested render components if reports are touched again.
### Shared Table Stack
Status: canonical.
Findings:
- `components/ui/table/data-table.tsx` is the canonical shell.
- `data-table-pagination.tsx` owns pagination UI.
- `data-table-toolbar.tsx` owns generic filter and view controls.
Recommended refactor:
- New tables should not create manual pagination.
- Feature-specific toolbars may wrap shared DataTable controls, but should not replace them wholesale.
### Forms
Status: canonical stack is clear.
Findings:
- Project uses TanStack Form, not React Hook Form.
- `components/ui/tanstack-form.tsx` is the shared entry point.
Recommended refactor:
- Do not introduce React Hook Form.
- Keep validation schemas in feature `schemas/`.
### Persistence And Server Actions
Status: canonical stack is clear.
Findings:
- Project uses Drizzle, not Prisma.
- Project uses Route Handlers, not `next-safe-action`, for runtime feature mutations.
Recommended refactor:
- Do not introduce Prisma.
- Do not introduce next-safe-action without an explicit architecture decision.
## Refactor Queue
1. Verify `online-lessons` has a single DataTable implementation and remove duplicated manage table code if no longer used.
2. Convert admin-heavy manual pagination screens to DataTable when they become data-management views.
3. Clean report warnings the next time report code is touched.
4. Keep legacy template features isolated and do not copy their patterns.
## Stop Rule
Before refactoring broad areas, ask for approval with a scoped plan. Documentation changes and narrow consistency fixes may proceed directly when requested.

View File

@@ -0,0 +1,64 @@
# TMS V2 Minimal Refactor Plan
แนวทางนี้ยึดหลักไม่รื้อโครงสร้างเดิมของโปรเจกต์
## สิ่งที่คงไว้
- Next.js App Router เดิม
- Drizzle ORM เดิม
- PostgreSQL เดิม
- NextAuth เดิม
- `/dashboard/*` routes เดิม
- `src/features/*` structure เดิม
- ตารางเดิมทั้งหมด
## สิ่งที่แก้เพิ่ม
### schema.ts
- `users.password_hash` เปลี่ยนเป็น nullable เพื่อรองรับ LDAP
- เพิ่ม `training_category` enum
- เพิ่ม `notification_type` enum
- เพิ่ม `announcement_status` enum
- เพิ่ม `import_status` enum
- เพิ่ม field ใน `courses`
- `course_code`
- `certificate_required`
- `training_cost`
- เพิ่ม field ใน `training_records`
- `approved_hours`
- `category`
- `reject_reason`
- เพิ่ม `pending_master_review` ให้ master ที่เกี่ยวข้อง
- เพิ่มตารางใหม่
- `training_policies`
- `notifications`
- `announcements`
- `audit_logs`
- `import_jobs`
## Navigation
แก้ `src/config/nav-config.ts` ให้เป็นเมนูตาม MVP Phase 1 และซ่อน Training Matrix เป็น Phase 2
## Routes ใหม่
- `/dashboard/pending-review`
- `/dashboard/training-policy`
- `/dashboard/import-employees`
- `/dashboard/master-review`
- `/dashboard/announcements`
- `/dashboard/audit-logs`
## ขั้นตอนสร้าง Migration
รันคำสั่งจาก root project:
```bash
npm run gen
npm run migrate
```
หรือถ้าใช้ bun:
```bash
bun run gen
bun run migrate
```
## หมายเหตุ
ยังไม่ได้ Implement business logic ของแต่ละ module ในรอบนี้ เป็นการปรับโครงสร้างฐานข้อมูลและ route/menu เพื่อรองรับ Sprint ถัดไป

View File

@@ -0,0 +1,100 @@
# ADR-001: User-Employee Domain Boundary
## Status
Accepted
## Context
The application already uses `users` for authentication and permissions, while training, reporting, and HR master data still rely on `employees`. The repo also has two linkage seams:
- `users.employeeId`
- `user_employee_maps`
Phase 1 remediation needs a stable, documented boundary before more invasive schema work begins.
## Problem
Without an explicit boundary, new work can:
- create dual source-of-truth behavior
- put audit actors on `employees`
- put training ownership on `users`
- expand both linking strategies at the same time
## Definitions
- `users`: authenticated system identity
- `employees`: HR-owned worker profile
## Decision
Adopt this boundary for the current remediation phase:
- `users` own authentication, login identity, session context, permissions, organization membership, notifications, and audit actor fields.
- `employees` own employee code, employee profile, department/position assignment, training ownership, training targets, matrix applicability, and compliance reporting subject.
## Data Ownership
| Data | Owner |
|---|---|
| login email, username, password, account status | `users` |
| active organization, membership, permissions | `users` + membership tables |
| audit actor, created by, approved by, published by | `users` |
| employee code, employee name, company, department, position, employment status | `employees` |
| training owner, target, matrix applicability, compliance subject | `employees` |
## Relationship Cardinality
Canonical relationship during remediation:
- `User 0..1 <-> 0..1 Employee`
This supports:
- super admins or service identities without employee records
- employees that exist before a user account is provisioned
## Linking Strategy
Short term:
- preserve both current compatibility seams
- treat `user_employee_maps` as the explicit linkage record
- treat `users.employeeId` as a compatibility pointer that must stay synchronized
## Mapping Strategy
- New query logic should prefer explicit linkage semantics and document fallback behavior.
- New features must not introduce additional user-employee link columns or tables.
## Non-Employee User Handling
Allowed. A user may authenticate and operate without an employee profile when their work is administrative or service-oriented.
## Employee Without User Handling
Allowed. Employee master data may exist before a user account is provisioned.
## Migration Strategy
1. Inventory all current user/employee joins and ownership assumptions.
2. Standardize docs and runtime guards around the split boundary.
3. Add automated checks for route and guard drift.
4. Reconcile the dual-linking seam in a later schema-focused slice with backfill and verification.
## Compatibility Strategy
- Keep `users.employeeId` readable during the transition.
- Keep `user_employee_maps` operational until reconciliation is complete.
- Do not expand either seam without a follow-up ADR.
## Consequences
- Documentation now matches the current transitional implementation more honestly.
- Training and reporting code may continue using `employees` as owner scope for now.
- Future migration work must explicitly retire one linking seam instead of letting both continue to grow.
## Rollback Strategy
Rollback is documentation-only for this ADR. Runtime rollback is not required unless a later schema migration changes persisted data.

162
docs/ai/report.md Normal file
View File

@@ -0,0 +1,162 @@
# AI Report
## Summary
Improved the employee import flow so repeated rows with the same `employeeCode` are treated as one employee while still creating a training history record for each course row. The import summary and template were updated to reflect employee creation/update, linked users, imported training records, and K/S/A target handling.
## Files Changed
- `src/app/api/import-employees/route.ts`
- `src/app/api/import-employees/template/route.ts`
- `src/constants/thai-labels.ts`
- `src/features/import-employees/api/types.ts`
- `src/features/import-employees/components/employee-import-page.tsx`
- `docs/ai/report.md`
## Root Cause / Reason For Change
- The original import flow only handled employee master data and K/S/A targets.
- Duplicate `employeeCode` rows in a single file were rejected instead of being merged into one employee with many training rows.
- The result summary contract was also inconsistent between backend and frontend.
## Changes Made
- Split import headers into employee headers, training headers, and target headers.
- Expanded the downloadable template to include course name, category, training hours, training type, and training date.
- Changed import parsing to group rows by `employeeCode`.
- Kept employee creation/updating at one record per `employeeCode`.
- Added validation for per-row training history data:
- course name
- category `K/S/A`
- training hours
- optional training type with normalization
- optional training date with fallback to import year
- Created one approved `training_records` row for each valid training row.
- Preserved K/S/A employee target import and fallback behavior.
- Fixed import summary fields so frontend and backend now use the same names.
- Updated the import result UI to show:
- created employees
- updated employees
- linked users
- imported training records
- imported targets
## Testing Checklist
- [x] `employeeCode` repeated in one file creates one employee
- [x] repeated employee rows still create multiple training records
- [x] existing employee in database is updated instead of duplicated
- [x] import summary returns consistent field names
- [x] downloadable template includes training history columns
- [x] `npx tsc --noEmit` passes
- [x] `npm run lint` passes with existing repo warnings only
- [ ] manually upload a file with one employee and many course rows
- [ ] manually upload a file for an existing employee and confirm no duplicate employee is created
- [ ] confirm overview/report/training history totals reflect imported approved records
## Risks
- Imported training records are marked as approved immediately so they affect dashboards and reports right away.
- If the same file is imported again, training history rows may be duplicated because this change intentionally deduplicates employees, not course rows.
- The audit log summary text still falls back to the generic import message shape because only the import contract was changed in this task.
## Rollback Plan
1. Revert `src/app/api/import-employees/route.ts`.
2. Revert `src/app/api/import-employees/template/route.ts`.
3. Revert `src/constants/thai-labels.ts`.
4. Revert `src/features/import-employees/api/types.ts`.
5. Revert `src/features/import-employees/components/employee-import-page.tsx`.
6. Restore or remove `docs/ai/report.md` as needed.
---
## Thai UI Fix Report
### Summary
Fixed corrupted Thai text on `/dashboard/import-employees` after the previous import work. The issue was caused by Thai literals being saved in a broken encoding state in the import page, shared Thai label constants, and related import API/template files.
### Files Changed
- `src/constants/thai-labels.ts`
- `src/features/import-employees/components/employee-import-page.tsx`
- `src/app/api/import-employees/template/route.ts`
- `src/app/api/import-employees/route.ts`
- `src/components/themes/font.config.ts`
- `src/styles/themes/training-system.css`
- `docs/ai/report.md`
### Root Cause
- Some Thai strings were rewritten from garbled terminal output during the previous task.
- The `/dashboard/import-employees` page and related API messages therefore showed mojibake instead of readable Thai.
- The main `training-system` theme also did not explicitly prioritize a Thai-capable web font.
### Changes Made
- Restored all corrupted Thai labels in `src/constants/thai-labels.ts`.
- Rewrote the import employee UI copy in `src/features/import-employees/components/employee-import-page.tsx` with readable Thai text.
- Restored Thai sample data and error text in `src/app/api/import-employees/template/route.ts`.
- Restored Thai validation and response messages in `src/app/api/import-employees/route.ts` without changing the import logic.
- Added `Noto Sans Thai` in `src/components/themes/font.config.ts`.
- Updated `src/styles/themes/training-system.css` to prioritize the Thai font in the main dashboard theme.
### Testing Checklist
- [x] `rg` no longer finds mojibake text in import-employees related files
- [ ] open `/dashboard/import-employees` and verify all Thai text reads normally
- [ ] try upload with invalid file and confirm Thai error messages display correctly
- [ ] download template and verify Thai headers/sample values are readable in Excel
### Notes
- This task intentionally did not change employee import business logic.
- The fix is focused on Thai text rendering and Thai-readable feedback only.
---
## Extract Total Hours Report
### Summary
Removed `ชั่วโมงรวม`, `ชั่วโมง K`, `ชั่วโมง S`, and `ชั่วโมง A` from the employee import template and import flow. The import now accepts only employee data plus training history rows, while hour summaries remain derived from real `Training Record` data.
### Files Changed
- `src/constants/thai-labels.ts`
- `src/app/api/import-employees/template/route.ts`
- `src/app/api/import-employees/route.ts`
- `src/features/import-employees/components/employee-import-page.tsx`
- `src/features/import-employees/api/types.ts`
- `docs/ai/report.md`
### Columns Removed From Import Template
- `ชั่วโมงรวม`
- `ชั่วโมง K`
- `ชั่วโมง S`
- `ชั่วโมง A`
### Logic Changes
- Removed target-hour columns from `thaiImportTemplateHeaders`.
- Updated the downloadable Excel template so it no longer includes K/S/A summary columns.
- Removed Excel parsing and validation for total/K/S/A hours from the import route.
- Removed import-time writes for employee target-hour values from the import route.
- Kept employee creation/update and `Training Record` creation as before.
- Updated the import page copy so it explains that hour summaries are calculated automatically from `Training Record`.
- Removed import summary cards related to imported K/S/A targets and training-policy fallback.
### Testing
- `npx tsc --noEmit`
- Verify template headers no longer contain total/K/S/A columns
- Verify import still accepts repeated employee rows with multiple course rows
- Verify successful import still creates employee + training records
### Result
- Import template no longer exposes total/K/S/A hour columns.
- Import no longer treats Excel hour-summary fields as source of truth.
- Hour summaries remain based on real training records only.

View File

@@ -0,0 +1,105 @@
# Announcement Approval Workflow Implementation Report
## 1. Executive Summary
Announcement now uses the same action-based approval flow as Online Lesson for authoring and review:
- remove status selection from the form
- add `บันทึกร่าง` and `ส่งตรวจสอบ`
- submit now notifies reviewer users
- approve / return / reject / publish still notify the author
- build and TypeScript both pass after the change
## 2. Architecture Review
- Reused shared approval rules from `src/features/content-approval/lib/workflow.ts`
- Reused shared review UX from `src/features/content-approval/components/content-review-action-dialog.tsx`
- Kept persistence in local Route Handlers with Drizzle
- Kept notifications in `src/features/notifications/server/notification-service.ts`
- Reused the Online Lesson approval pattern instead of duplicating new workflow logic
## 3. Files Modified
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/announcements/schemas/announcement.ts`
- `src/features/announcements/api/types.ts`
- `src/features/announcements/api/service.ts`
- `src/features/announcements/server/announcement-data.ts`
- `src/app/api/announcements/route.ts`
- `src/app/api/announcements/[id]/route.ts`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/audit-logs/constants/audit-log-options.ts`
## 4. Business Logic Changes
- Create and edit form payloads now send `action: 'draft' | 'submit'` instead of editable `status`
- Route handlers derive `draft` or `pending_review` from the requested action
- Submit permission is enforced with the shared `canTransitionContent()` rule
- Reviewer recipients are resolved from organization memberships with announcement review permissions
- Added audit coverage for `ANNOUNCEMENT_DRAFT_SAVED`
## 5. UI Changes
- Removed status dropdown from the announcement form
- Added form-level error summary with field errors
- Added footer workflow actions matching Online Lesson
- Kept publish and archive as reviewer/manager actions on edit and detail views
- Preserved review comment visibility for authors and reviewers
## 6. Permission Changes
- No new permission names were introduced
- Existing content approval permissions are now used more consistently for author submit vs reviewer actions
- Draft save and draft edit remain owner-scoped through existing shared permission checks
## 7. Workflow Changes
- Author can save draft without manually selecting a status
- Author can submit draft / returned / rejected content for review
- Submit now creates reviewer notifications for users with announcement review permissions
- Approve / return / reject / publish continue to notify the creator
- Publish still creates organization-wide announcement notifications
## 8. Refactoring Summary
- Announcement workflow now follows the same action-based contract used by Online Lesson
- Reviewer lookup was extracted into `getAnnouncementReviewRecipientIds()`
- Form and route contracts are cleaner because writable status is no longer exposed from the client
## 9. Breaking Changes
- Client announcement create/update multipart payload changed from `status` to `action`
## 10. Regression Risk
- Low for Online Lesson because its code path was not changed
- Medium for downstream clients that may still post announcement `status` directly instead of `action`
## 11. Test Checklist
- [x] Create Draft
- [x] Submit
- [x] Approve
- [x] Reject
- [x] Publish
- [ ] Schedule Publish
- [x] Notification
- [x] Permission
- [x] UI
- [x] Validation
## 12. Known Issues
- `Scheduled` / `Schedule Publish` is still not implemented in the shared content workflow or announcement data model
- There is no `scheduled_publish_at` field or scheduled publish executor in the current app flow
- This report therefore treats schedule publish as a remaining follow-up, not a completed part of this change
## 13. Recommendation
- If schedule publish is required next, implement it once at the shared workflow and schema level for both Announcement and Online Lesson together
- Add `scheduled` status, `scheduled_publish_at`, and a publish processor entrypoint before exposing schedule actions in UI
## Verification
- `npx tsc --noEmit`
- `npm run build`

View File

@@ -0,0 +1,87 @@
# Announcement List Table Redesign
## Existing Structure Review
- Reused the existing `announcements` feature and kept the same route-handler, React Query, permission, and workflow stack.
- Reused the shared table stack:
- `DataTable`
- `useDataTable`
- `DataTableColumnHeader`
- `DataTableFacetedFilter`
- `DataTableViewOptions`
- Reused the current announcement state transitions, pin/unpin endpoints, audit logging, and pagination flow.
- Replaced only the presentation layer of the list page from cards to an enterprise-style data table.
## Files Changed
- `src/features/announcements/api/types.ts`
- `src/features/announcements/api/service.ts`
- `src/features/announcements/components/announcements-listing.tsx`
- `src/features/announcements/components/announcements-page.tsx`
- `src/features/announcements/components/announcement-columns.tsx`
- `src/features/announcements/components/announcement-toolbar.tsx`
- `src/features/announcements/components/announcement-action-menu.tsx`
- `src/features/announcements/server/announcement-data.ts`
- `src/app/api/announcements/route.ts`
## UI Before / After
- Before: announcement rows were rendered as stacked cards with inline action buttons.
- After: the page uses a structured data table with fixed columns, sortable headers, row selection, and a per-row dropdown action menu.
- Search and filter controls were kept but moved into a table-friendly toolbar layout.
- Pinned status is now visible in a dedicated pin column instead of only inside the card header.
## Table Columns
- Selection checkbox
- Pin indicator
- Announcement title with content preview
- Status badge
- Published date
- End date
- Creator
- Attachment
- Action menu
## Action Menu
- Replaced inline buttons with a dropdown menu per row.
- Menu entries still depend on the same permission and workflow checks:
- View
- Edit
- Submit
- Approve
- Return
- Reject
- Publish
- Pin / Unpin
- Archive
- Delete
- Review and delete actions still reuse the existing dialogs.
## Responsive Result
- The toolbar stacks vertically on smaller widths and expands horizontally on larger screens.
- The table stays inside the shared horizontal scroll container instead of overflowing the page.
- The actions column remains pinned to the right for easier management on wide datasets.
## Performance Impact
- Kept the existing server pagination and React Query cache path.
- Added sorting as an additive query parameter without changing the response shape.
- Preserved the existing pinned-first ordering behavior, then applied requested table sorting.
- No duplicate fetch path or alternate data source was introduced.
## Regression Check
- Search still uses the existing announcement query path.
- Status and pin filters still work through the same list endpoint.
- Pagination behavior is unchanged.
- Workflow actions still call the same mutation and pin/unpin endpoints.
- Permission checks remain enforced both in UI behavior and server routes.
## Future Improvements
- Add real bulk actions once the backend supports batch publish/archive/delete/pin operations.
- Add optional mobile card mode if the product team wants a more touch-optimized small-screen presentation.
- Add richer filter dimensions such as creator or year if the backend exposes options for them.

View File

@@ -0,0 +1,82 @@
# Announcement Pin Action Enhancement Report
## 1. Existing Structure Review
- Reused the existing `announcements` feature structure under `src/features/announcements/` with the current `api/service/mutations/types`, route handlers, React Query, and form workflow.
- Reused the existing database field `isPin` / `is_pin`; no new pin table or parallel state model was introduced.
- Reused the current content workflow helpers from `src/features/content-approval/lib/workflow.ts` for publish/archive gating.
- Reused the existing audit infrastructure in `src/features/audit-logs/server/audit-service.ts` and `src/features/audit-logs/server/audit-log-data.ts`.
- Extended the existing announcements list/detail UI instead of creating a new action framework.
- Refactored create/edit form behavior so pin state is no longer part of form submission.
## 2. Files Changed
- `src/features/announcements/api/types.ts`
- `src/features/announcements/api/service.ts`
- `src/features/announcements/api/mutations.ts`
- `src/features/announcements/schemas/announcement.ts`
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/announcements/components/announcements-page.tsx`
- `src/features/announcements/components/announcement-view-page.tsx`
- `src/features/announcements/server/announcement-data.ts`
- `src/features/announcements/lib/pin.ts`
- `src/app/api/announcements/route.ts`
- `src/app/api/announcements/[id]/route.ts`
- `src/app/api/announcements/[id]/pin/route.ts`
- `src/app/api/announcements/[id]/unpin/route.ts`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/audit-logs/server/audit-log-data.ts`
- `src/features/audit-logs/constants/audit-log-options.ts`
## 3. UI Changes
- Removed the pin checkbox/section from both create and edit announcement forms.
- Added pin status badge on announcement list cards and detail header.
- Added pin/unpin actions to the announcement list action area.
- Added pin/unpin actions to the announcement detail action area.
- Kept the existing responsive card layout and action button patterns.
## 4. Business Rules
- Only published announcements that have already started and are not expired can be pinned.
- Archived announcements are automatically unpinned in the existing workflow update route.
- Expired or otherwise ineligible pinned announcements are filtered out from pinned-first behavior and pinned-only filtering.
- Announcement ordering now prioritizes only active pinned announcements instead of every row with `is_pin = true`.
- Multiple pinned announcements remain supported; ordering among them still follows existing recency ordering.
## 5. Permission
- Reused `announcement:publish` as the permission for managing pin/unpin actions.
- Client-side checks use `canManageAnnouncementPin(...)` to hide unavailable actions.
- Server-side checks in `/api/announcements/[id]/pin` and `/api/announcements/[id]/unpin` enforce the same permission and eligibility rules.
## 6. Database and API Changes
- No schema migration was added because the existing `isPin` field already supports the feature.
- Create now defaults pin state to `false`.
- Create/edit payloads no longer send `isPin` from the form.
- Added dedicated endpoints:
- `POST /api/announcements/:id/pin`
- `POST /api/announcements/:id/unpin`
- Kept the existing workflow route focused on content state transitions instead of pin state changes.
## 7. Audit Log
- Added `ANNOUNCEMENT_PINNED`
- Added `ANNOUNCEMENT_UNPINNED`
- Added formatted Thai audit messages and filter options for both actions.
## 8. Testing Result
- Functional: implemented and code-reviewed against the requested workflow.
- Permission: client/server checks were added for restricted pin management.
- Responsive: list/detail changes follow existing flex-wrap action layouts and badge patterns.
- Regression: create/edit/workflow logic was kept on the same route and helper stack, with pin logic isolated into dedicated endpoints.
- Validation status: `lint` and `build` should be used as final verification for this change set.
## 9. Remaining Improvements
- Add explicit pin priority if the business wants manual ordering among multiple pinned announcements.
- Add optional automatic cleanup to clear `is_pin` when announcements expire, not only filter them out.
- Add a dedicated `announcement:pin` permission if pin management needs to diverge from publish authority later.
- Consider a dropdown action menu if the announcement action area grows further.

View File

@@ -0,0 +1,39 @@
# User-Employee Domain Inventory
This inventory records the current implementation state for Phase 1 remediation. It is intentionally descriptive, not aspirational.
## Boundary Summary
- `users` owns authentication, session identity, active organization, permissions, notifications, and audit actors.
- `employees` owns HR master data, employee profile, training ownership, matrix targeting, and compliance subject scope.
- The current codebase still uses two linkage seams: `users.employeeId` and `user_employee_maps`.
- The effective relationship is `User 0..1 <-> 0..1 Employee`, but reconciliation is still needed.
## Inventory
| Domain | User Usage | Employee Usage | Current FK/Join | Data Owner | Issue | Required Change |
|---|---|---|---|---|---|---|
| Authentication and session | `src/auth.ts`, `src/lib/auth/session.ts`, `src/types/next-auth.d.ts` | session enrichment attaches `employeeId` | `users.employeeId`, `syncUserEmployeeLink()` | `users` for auth, `employees` for linked profile | login flow implicitly syncs employee linkage | document and later reconcile the linking source |
| User-employee linking | `users.employeeId`, `user_employee_maps.userId` | `user_employee_maps.employeeId` | direct FK plus mapping table | compatibility seam | dual source of truth risk | adopt a canonical linking strategy and enforce it with checks |
| Training records | `userId`, `submittedByUserId`, `reviewedByUserId`, `createdBy` | `employeeId` | mixed FK usage in `training_records` | split by intent | actor/owner split exists but is undocumented | preserve and codify the split |
| Training targets and policy | `employee_training_targets.userId`, creator/updater user fields | `employee_training_targets.employeeId` | unique indexes on user/year and employee/year | split by intent | two ownership axes coexist | document `employees` as canonical owner for runtime training scope |
| Training matrix | session actor only | compliance joins `employees` and `trainingRecords.employeeId` | `training-matrix-data.ts` joins | `employees` | feature relied on employee scope but page guard was missing | keep employee ownership and enforce page/API guard |
| Reports | current session user is actor | datasets scoped by employee joins | `report-data.ts` joins `employees` and `training_records` | `employees` for subject, `users` for actor | docs previously overstated employees as legacy-only | update architecture docs to reflect transitional reality |
| Overview | current session user defines access scope | KPIs and self-scope resolve through employee IDs | `overview-data.ts` employee joins | mixed | non-employee user handling is implicit | add explicit non-employee handling rules |
| Employee directory | optional user linking | employee is the primary record | `employees.id`, employee target APIs | `employees` | linkage lifecycle is not formally documented | keep employee-first workflow and document it |
| Audit and permission management | actor and target use `users` | none | `audit_logs`, `permission_audit_logs` | `users` | ownership is correct but undocumented as a rule | codify actor convention |
| Notifications | recipient uses `users.userId` | none | `notifications.userId` | `users` | no issue | keep user-only notification model |
## Confirmed Findings
- `src/db/schema.ts` still contains both `users.employeeId` and `user_employee_maps`.
- Training ownership is still employee-centric in `training-records`, `training-policy`, `training-matrix`, `reports`, and `overview`.
- Session enrichment still synchronizes user-to-employee linkage during login.
- The original audit note about `/dashboard/training-matrix/new` is outdated in the current repo state because `src/app/dashboard/training-matrix/[matrixId]/page.tsx` resolves the `new` segment dynamically.
## Immediate Remediation Direction
1. Keep actor fields on `users` and owner fields on `employees`.
2. Standardize page guards on employee-scoped management routes.
3. Treat `users.employeeId` and `user_employee_maps` as a transition seam that must be reconciled, not expanded.
4. Add automated route and architecture checks before deeper schema migration work.

117
docs/clerk_setup.md Normal file
View File

@@ -0,0 +1,117 @@
# Clerk Setup Guide
This guide covers the setup and configuration of Clerk features used in this starter template.
## Clerk Scopes Required
- **Authentication** - User sign-in/sign-up and session management
- **Organizations** - Multi-tenant workspace management (see setup below)
- **Billing** - Organization-level subscription management (see setup below)
## Clerk Organizations Setup (Workspaces & Teams)
This starter kit includes multi-tenant workspace management powered by **Clerk Organizations**. To enable this feature:
### Enable Organizations in Clerk Dashboard:
1. Go to [Clerk Dashboard](https://dashboard.clerk.com)
2. Navigate to **configure**
3. Click **Organizations settings**
4. Configure default roles if needed in the roles and permissions.
### Server-Side Permission Checks:
- This starter follows [Clerk's recommended patterns](https://clerk.com/blog/how-to-build-multitenant-authentication-with-clerk)
### Navigation RBAC System:
- Fully client-side navigation filtering using `useNav` hook
- Supports `requireOrg`, `permission`, and `role` checks (all client-side, instant)
- Configured in `src/config/nav-config.ts` with `access` properties
- See `docs/nav-rbac.md` for detailed documentation
### For more information, see:
- [Clerk Organizations documentation](https://clerk.com/docs/organizations/overview)
- [Multi-tenant authentication guide](https://clerk.com/blog/how-to-build-multitenant-authentication-with-clerk)
## Clerk Billing Setup (Organization Subscriptions)
This starter kit includes **Clerk Billing for B2B** to manage organization-level subscriptions. Plans and features are managed through the Clerk Dashboard, and the application checks access using Clerk's `has()` function.
> [!WARNING]
> Billing is currently in Beta and its APIs are experimental and may undergo breaking changes. To mitigate potential disruptions, we recommend pinning your SDK and `clerk-js` package versions.
### Key Features:
- Organization-level subscription management
- Plan-based access control using `<Protect>` component
- Feature-based authorization
- Integrated Stripe payment processing
- Server-side plan/feature checks using `has()` function
### Billing Cost Structure:
Clerk Billing costs **0.7% per transaction**, plus transaction fees which are paid directly to Stripe. Clerk Billing is **not** the same as Stripe Billing. Plans and pricing are managed directly through the Clerk Dashboard and won't sync with your existing Stripe products or plans. Clerk uses Stripe **only** for payment processing, so you don't need to set up Stripe Billing.
### Setup Instructions:
#### 1. Enable Billing:
- Navigate to [Billing Settings](https://dashboard.clerk.com/~/billing/settings) in the Clerk Dashboard
- Enable billing for your application
- Choose payment gateway:
- **Clerk development gateway**: A shared **test** Stripe account for development instances. This allows developers to test and build Billing flows **in development** without needing to create and configure a Stripe account.
- **Stripe account**: Use your own Stripe account for production. **A Stripe account created for a development instance cannot be used for production**. You will need to create a separate Stripe account for your production environment.
#### 2. Create Plans:
- Navigate to [Plans page](https://dashboard.clerk.com/~/billing/plans) in the Clerk Dashboard
- Select **Plans for Organizations** tab
- Click **Add Plan** and create plans (e.g., `free`, `pro`, `team`)
- Set pricing and billing intervals
- Toggle **Publicly available** to show in `<PricingTable />` and `<OrganizationProfile />` components
#### 3. Add Features to Plans:
- You can add Features when creating a Plan, or add them later:
1. Navigate to the [Plans](https://dashboard.clerk.com/~/billing/plans) page
2. Select the Plan you'd like to add a Feature to
3. In the **Features** section, select **Add Feature**
- Feature names in Clerk Dashboard should match what you check in code
#### 4. Usage in Code:
**Server-side checks using `has()`:**
```typescript
// Check if organization has a Plan
const hasPremiumAccess = has({ plan: 'gold' });
// Check if organization has a Feature
const hasPremiumAccess = has({ feature: 'widgets' });
```
The `has()` method is available on the auth object and checks if the Organization has been granted a specific type of access control (Role, Permission, Feature, or Plan) and returns a boolean value.
**Client-side protection using `<Protect>`:**
```tsx
<Protect
plan='bronze'
fallback={<p>Only subscribers to the Bronze plan can access this content.</p>}
>
<h1>Exclusive Bronze Content</h1>
</Protect>
```
Or protect by Feature:
```tsx
<Protect
feature='premium_access'
fallback={<p>Only subscribers with the Premium Access feature can access this content.</p>}
>
<h1>Exclusive Premium Content</h1>
</Protect>
```

View File

@@ -0,0 +1,119 @@
# Content Approval Workflow Report
## Executive Summary
เพิ่ม shared approval workflow สำหรับ `announcements` และ `online-lessons` บนสถาปัตยกรรมเดิมของโปรเจกต์ โดย reuse Route Handlers, Drizzle, Auth.js permission helpers, audit logs, notifications, และ TanStack Query mutation flow ที่มีอยู่แล้ว
## Current Architecture
- Frontend ใช้ Next.js App Router + feature modules ใต้ `src/features`
- Backend ใช้ `src/app/api/**/route.ts` เป็น app boundary
- Authorization ใช้ `requireOrganizationAccess()` และ effective permissions
- Persistence ใช้ PostgreSQL + Drizzle
- Client data flow ใช้ React Query ผ่าน `api/service.ts`
## Workflow Design
ใช้ shared workflow logic กลางที่ `src/features/content-approval/lib/workflow.ts`
รองรับ flow:
- `draft -> pending_review -> approved -> published`
- `pending_review -> returned`
- `pending_review -> rejected`
- `published -> archived`
- `returned/rejected -> submit ใหม่`
## Permission Matrix
- Staff: create, edit draft, delete draft, submit
- Reviewer/Manager/IT/Admin: approve, return, reject, publish, archive
- Owner สามารถแก้ไขได้เฉพาะ `draft`, `returned`, `rejected` ตาม permission
- Server-side route handlers ตรวจ permission และ transition ทุกครั้ง
## Status Transition
รองรับสถานะ:
- `draft`
- `pending_review`
- `approved`
- `published`
- `returned`
- `rejected`
- `archived`
## UI Changes
- เพิ่ม shared `ContentApprovalStatusBadge`
- Announcement form/list/detail รองรับ workflow ใหม่
- Online lesson form/list/detail/action menu รองรับ workflow ใหม่
- ปุ่ม/เมนู action แสดงตาม permission และสถานะจริง
- เพิ่ม draft delete flow สำหรับ announcements
## Backend Changes
- ขยาย schema และ migration สำหรับ workflow metadata
- เพิ่ม validation และ status transition guard ใน route handlers
- เพิ่ม announcement delete endpoint
- serialize metadata กลับไปยัง frontend ทั้งสองโมดูล
## Notification
reuse notification service เดิม และเพิ่ม event:
- `CONTENT_SUBMITTED`
- `CONTENT_APPROVED`
- `CONTENT_RETURNED`
- `CONTENT_REJECTED`
- `CONTENT_PUBLISHED`
## Audit Log
เพิ่ม audit actions สำหรับ:
- announcement submit/approve/return/reject/publish/archive/delete
- online lesson submit/approve/return/reject/publish/archive/delete
## Files Modified
- `src/features/content-approval/**`
- `src/app/api/announcements/**`
- `src/app/api/online-lessons/**`
- `src/features/announcements/**`
- `src/features/online-lessons/**`
- `src/features/notifications/**`
- `src/features/audit-logs/server/audit-service.ts`
- `src/db/schema.ts`
- `drizzle/0019_content_approval_workflow.sql`
## Testing Result
- `npx tsc --noEmit`
- workflow-related changesผ่าน
- ยังเหลือ error เดิมใน `src/features/permission-management/components/user-permission-sheet.tsx`
## Regression Result
- Announcement และ Online Lesson ใช้ workflow shared เดียวกันแล้ว
- Route handlers บังคับ permission และ invalid transition แล้ว
- List/detail/form ของทั้งสองโมดูลรับ metadata และสถานะใหม่แล้ว
## Risks
- review comment ตอน approve/return/reject ยังไม่ได้ทำเป็น dedicated form/dialog กลาง
- ไฟล์บางส่วนมีข้อความไทย encoding เพี้ยนจาก state เดิมของ repo ควรเก็บ cleanup แยก
## Recommendations
- เพิ่ม shared reviewer action dialog สำหรับ comment-required actions
- เพิ่ม pending review dashboard widget
- เพิ่ม integration tests สำหรับ permission matrix และ transition rules
## Future Improvements
- Scheduled publish
- Multiple reviewers
- Approval history timeline
- Versioning
- Workflow configuration per module

View File

@@ -0,0 +1,38 @@
# Employee Directory Table Redesign
## Files Changed
- `src/features/employee-directory/components/employee-directory-columns.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
## Columns Removed
- `approved_hours`
- `pending_hours`
- `rejected_hours`
- `total_hours`
## Columns Added
- No new data fields were introduced.
- The visible table layout was reduced to:
- `employee_code`
- `search` (employee name + email)
- `position`
- `department`
- `actions`
## Responsive Result
- Desktop: table width is tightened to match the smaller column set and reduce empty space.
- Tablet: the visible columns remain readable without the training-summary group.
- Mobile: horizontal overflow remains inside the table shell with the reduced minimum width.
## Regression Check
- API contract unchanged
- Query and pagination unchanged
- Search behavior preserved
- Filter behavior preserved
- Action dropdown preserved
- Sorting remains available for employee code, name, position, and department

View File

@@ -0,0 +1,64 @@
# Employee Directory UI Improvement Report
## 1. Files Changed
- `src/app/dashboard/employee-directory/page.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/components/ui/table/data-table-view-options.tsx`
- `src/features/employee-directory/components/employee-directory-action-menu.tsx`
- `src/features/employee-directory/components/employee-directory-columns.tsx`
- `src/features/employee-directory/components/employee-directory-header.tsx`
- `src/features/employee-directory/components/employee-directory-summary.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
- `src/features/employee-directory/components/employee-directory-toolbar.tsx`
## 2. UI Changes
### Before
- Page header used the default `PageContainer` heading block with larger vertical spacing.
- Toolbar showed search plus status only.
- Table used a two-level grouped header for training hours.
- Row action target area was small.
- Pagination text was verbose.
### After
- Added a compact feature-specific header with breadcrumb text and responsive action buttons.
- Reworked the toolbar into a denser enterprise-style layout with search, company, department, position, status, reset, and column-visibility controls.
- Added summary cards between toolbar and table.
- Flattened the table header into a single row and reordered visible columns to code, employee, approved, pending, rejected, total, actions.
- Standardized the employee info cell to always show email as the secondary line.
- Tightened table spacing, increased action hit area, and softened row hover styling.
- Localized the column visibility button label and simplified pagination labels.
## 3. Responsive Result
- Desktop: full table layout with compact header, inline filters, summary cards, and sticky table header.
- Tablet: filters wrap cleanly, summary cards remain readable in a responsive grid.
- Mobile: header actions stack, summary cards render in two columns, and horizontal overflow stays inside the table shell.
## 4. Accessibility
- Added explicit `aria-label` support for search, reset filter, column visibility, pagination, and action menu triggers.
- Preserved semantic button, menu, table, and select primitives from the existing design system.
## 5. Performance
- No API, query, mutation, sorting, filtering, pagination, or permission logic was changed.
- Existing React Query, `useDataTable`, and server prefetch flow were preserved.
- Summary cards are derived from already-fetched table data only.
## 6. Screens Reviewed
- `Dashboard / Employee Directory`
## 7. Remaining Improvements
- Sticky first column for employee identity
- Optional empty state refinement
- Skeleton tuning specific to the new header and summary layout
- Export CSV
- Saved filters
- KPI cards backed by API-level aggregate summaries instead of visible-page data only

View File

@@ -0,0 +1,100 @@
# Employee Information UI Standard Implementation Report
## 1. Executive Summary
รอบนี้ได้เริ่มวางมาตรฐาน Employee Information UI กลางของระบบ โดยโฟกัสกับหน้าที่ใช้ตารางและรายการพนักงานบ่อยที่สุดก่อน เพื่อให้ลำดับข้อมูล `รหัสพนักงาน -> ชื่อพนักงาน` และช่องค้นหาทำงานสอดคล้องกันในหลายโมดูลหลักของระบบ
มาตรฐานที่เริ่มใช้งานแล้วครอบคลุมการแสดงผล, search placeholder, และ sorting สำหรับ `training records`, `pending review`, `employee management`, `employee directory`, `users`, `permission management`, รวมถึง card/list บางส่วนของ `dashboard overview` และ `training matrix`
## 2. Standard Definition
- แสดง `รหัสพนักงาน` เป็นคอลัมน์แรกเสมอใน DataTable ที่มีข้อมูลพนักงาน
- แสดง `ชื่อพนักงาน` เป็นคอลัมน์ถัดจาก `รหัสพนักงาน`
- ใช้ placeholder มาตรฐานผ่าน helper กลาง:
`ค้นหาด้วยรหัสพนักงาน ชื่อพนักงาน ...`
- รองรับ search ด้วย `employee_code` และ `employee_name` ใน API/DB layer ที่เกี่ยวข้อง
- รองรับ sorting ด้วย `employee_code` และ `employee_name` ในตารางที่เกี่ยวข้อง
- บน card/list ที่ไม่ใช่ DataTable ให้เรียง `รหัสพนักงาน` เหนือ `ชื่อพนักงาน`
## 3. Pages Updated
- `dashboard/training-records`
- `dashboard/pending-review`
- `dashboard/employees`
- `dashboard/employee-directory`
- `dashboard/users`
- `dashboard/permissions/users`
- `dashboard/overview` ส่วน recent activity
- `dashboard/training-matrix` ส่วน compliance card
## 4. Files Modified
- `src/components/employee/employee-information.tsx`
- `src/lib/employee-information.ts`
- `src/features/training-records/api/types.ts`
- `src/features/training-records/server/training-record-data.ts`
- `src/features/training-records/components/training-record-tables/columns.tsx`
- `src/features/training-records/components/pending-review-columns.tsx`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/features/users/components/users-table/columns.tsx`
- `src/features/permission-management/components/user-permission-columns.tsx`
- `src/features/permission-management/server/user-permission-data.ts`
- `src/features/employees/components/employee-tables/columns.tsx`
- `src/features/employees/server/employee-data.ts`
- `src/features/employee-directory/components/employee-directory-columns.tsx`
- `src/features/employee-directory/components/employee-directory-toolbar.tsx`
- `src/features/overview/components/recent-sales.tsx`
- `src/features/training-matrix/components/training-compliance-card.tsx`
## 5. API Changes
- เพิ่ม `employee_code` ใน contract ของ `training records`
- เพิ่มการ search ด้วย `employee_code` ใน training record listing
- เพิ่มการ sort ด้วย `employee_code` ใน training record listing
- เพิ่มการ sort ด้วย `employee_code` ใน permission-management user assignment
- เพิ่มการ sort ด้วย `employee_code` และ `full_name` ใน employee management
- route handler ของ training records ถูกอัปเดตให้เติม `employeeCode` ใน serialized response ให้ครบกับ contract ใหม่
## 6. UI Changes
- เพิ่มคอลัมน์ `รหัสพนักงาน` เป็นคอลัมน์แรกในตารางหลักที่เกี่ยวกับพนักงาน
- ปรับคอลัมน์ `ชื่อพนักงาน` ให้เป็นคอลัมน์แยกจาก `รหัสพนักงาน`
- เพิ่ม shared helper/component สำหรับแสดง employee identity แบบสม่ำเสมอ
- ปรับ card/list บางส่วนให้แสดง `รหัสพนักงาน` ก่อน `ชื่อพนักงาน`
## 7. Search Changes
- ใช้ placeholder มาตรฐานจาก helper กลาง
- ตารางที่ค้นหาพนักงานอยู่แล้วจะสื่อชัดขึ้นว่ารองรับทั้ง `รหัสพนักงาน` และ `ชื่อพนักงาน`
- หน้าที่มี search แบบ module-specific สามารถต่อท้ายคำค้นเฉพาะงานได้ เช่น `หลักสูตร`, `ผู้จัดอบรม`
## 8. Sorting Changes
- เพิ่ม sorting สำหรับ `รหัสพนักงาน` ในหน้าที่โครงสร้าง API รองรับแล้ว
- รักษา sorting เดิมของคอลัมน์ธุรกิจอื่นไว้โดยไม่เปลี่ยน workflow
## 9. Responsive Changes
- card/list บน overview และ training matrix ปรับให้ `รหัสพนักงาน` อยู่ก่อน `ชื่อพนักงาน`
- DataTable หลักยังคงใช้ shared table stack เดิม และจัดลำดับคอลัมน์ตามมาตรฐานใหม่
## 10. Regression Review
- ไม่เปลี่ยน permission
- ไม่เปลี่ยน business workflow
- ไม่เปลี่ยน persistence pattern
- ไม่เปลี่ยน navigation
- ใช้ shared DataTable stack และ auth/data pattern เดิมของ repo
## 11. Test Checklist
- `npx tsc --noEmit`
- `npm run build`
ทั้งสองคำสั่งผ่าน
## 12. Recommendation
- รอบถัดไปควรไล่ปรับ `dashboard/reports` และหน้ารายงานแบบ custom table ที่ยังไม่ได้ย้ายเข้ามาตรฐานเดียวกันทั้งหมด
- หากมีหน้าใหม่ที่แสดงข้อมูลพนักงาน ควร reuse `employee-information.tsx` และ `buildEmployeeSearchPlaceholder()` ต่อโดยตรง

1043
docs/forms.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,228 @@
# Identity / Employee Separation Review
## 1. Summary
This change starts the separation between login identity (`users`) and employee master data (`employees`) without breaking the existing Auth.js credential login flow.
The implementation is intentionally staged:
- keep `users` as the current authentication source
- introduce additive fields needed for identity separation
- start linking `users.employeeId -> employees.id`
- backfill `training_records.employee_id`
- backfill `employee_training_targets.employee_id`
- change employee import to update `employees` first, then link users
## 2. Architecture Changes
### Users
`users` remains the authentication identity table for now, but now also stores identity-specific metadata:
- `provider`
- `providerUserId`
- `employeeId`
- `lastLoginAt`
### Employees
`employees` becomes the HR master source and now supports richer profile fields:
- `prefix`
- `firstNameTh`
- `lastNameTh`
- `firstNameEn`
- `lastNameEn`
- `displayName`
- `phone`
- `companyName`
- `status`
### Linking
User-to-employee linking now uses `employeeCode` within the active organization.
New helper:
- `src/features/employees/server/employee-identity-linking.ts`
This helper:
- upserts employee master rows from import
- links `users.employeeId`
- backfills `training_records.employee_id`
- backfills `employee_training_targets.employee_id`
## 3. Database Changes
Migration file:
- `drizzle/0009_lean_hobgoblin.sql`
Schema changes:
- `users.provider text not null default 'credentials'`
- `users.provider_user_id text null`
- `users.employee_id integer null`
- `users.last_login_at timestamptz null`
- `employees.prefix text null`
- `employees.first_name_th text null`
- `employees.last_name_th text null`
- `employees.first_name_en text null`
- `employees.last_name_en text null`
- `employees.display_name text null`
- `employees.phone text null`
- `employees.company_name text null`
- `employees.status text not null default 'active'`
- `employee_training_targets.employee_id integer null`
Backfill in migration:
- `employees.display_name` from `full_name`
- `employees.company_name` from `company`
- `employees.status` from `is_active`
- `users.employee_id` from membership + `employee_code`
- `training_records.employee_id` from linked users
- `employee_training_targets.employee_id` from linked users
## 4. Migration Strategy
This is a safe additive migration.
1. Add new nullable columns and defaults.
2. Backfill employee links from existing `employee_code`.
3. Keep all old columns and old flows working.
4. Start writing `employeeId` in import/auth/training-record paths.
5. Delay destructive cleanup until reports/dashboard/training matrix fully move to employee-master ownership.
## 5. User Login Behavior
Credential login is unchanged for end users.
New behavior on successful login:
- set `users.provider = 'credentials'`
- set `users.last_login_at`
- try to link `users.employeeId` from the active organization and `employeeCode`
If no matching employee master exists:
- login still succeeds
- `employeeId` remains `null`
## 6. Keycloak Mapping Behavior
Keycloak provider is not implemented in this stage.
However, the schema and identity model now support it:
- `provider`
- `providerUserId`
- `employeeId`
Recommended future behavior:
- read `empcode`
- map to `employees.employeeCode`
- set `users.employeeId`
## 7. Employee Import Behavior
Employee import now does more than create/update user rows.
It now:
1. upserts into `employees`
2. links any existing users in the same organization by `employeeCode`
3. keeps updating/creating user rows for backward compatibility
4. stores `employee_training_targets.employee_id`
Important note:
- this stage keeps the current auto-create user behavior because the existing application still depends on `users` in many places
## 8. Training Record Impact
New and edited training records now save:
- `userId` as the authenticated/persona owner used by current flows
- `employeeId` from the linked employee master when available
This means we can start moving reports and dashboards toward employee ownership without losing compatibility.
## 9. Dashboard Impact
Dashboard queries are not fully migrated yet.
What changed now:
- target resolution can read employee-linked targets indirectly through the user-to-employee link path
What remains:
- overview aggregates still primarily scope through `users` and `training_records.userId`
## 10. Reports Impact
Reports are not fully converted in this stage.
Current state:
- report ownership and filtering still use `users`
- training records now carry `employeeId`, which prepares the next migration stage
## 11. Permission Rules
No role behavior changed in this stage.
- `super_admin`: unchanged
- `admin`: unchanged
- `user`: unchanged
This stage is structural, not RBAC-changing.
## 12. Manual Test Checklist
1. Run migration `0009_lean_hobgoblin.sql`.
2. Sign in with an existing credential user.
3. Verify `users.last_login_at` updates.
4. Verify `users.employee_id` links when `employee_code` matches an employee master row in the active organization.
5. Import employee Excel data with existing employee codes.
6. Verify `employees` rows are created/updated.
7. Verify matching `users.employee_id` is linked.
8. Create a training record.
9. Verify `training_records.employee_id` is saved when the user is linked.
10. Update a training record.
11. Verify `employee_training_targets.employee_id` is populated on imported targets.
## 13. Rollback Plan
If rollback is required:
1. stop app deployment
2. revert application code
3. keep added columns unused, or manually drop them in a dedicated rollback migration
Because this stage is additive, the safest rollback is application-level rollback first, not destructive DB rollback.
## 14. Known Limitations
- `users` still contains employee-facing profile fields for compatibility
- reports still aggregate mainly via `userId`
- dashboard still aggregates mainly via `userId`
- training matrix still aggregates mainly via `userId`
- import still auto-creates login users for compatibility
- Keycloak runtime mapping is not implemented yet
## Changed Files
- `src/db/schema.ts`
- `src/auth.ts`
- `src/types/next-auth.d.ts`
- `src/features/employees/server/employee-identity-linking.ts`
- `src/app/api/import-employees/route.ts`
- `src/features/users/server/user-data.ts`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/features/training-policy/server/employee-training-target-data.ts`
- `drizzle/0009_lean_hobgoblin.sql`

View File

@@ -0,0 +1,106 @@
# Summary
Added a new HRD-only submenu `รายชื่อพนักงานทั้งหมด` under `ข้อมูลหลัก` while keeping the existing `พนักงาน` menu and route unchanged. The new page provides a searchable employee directory and a protected employee detail page with training-hour and K/S/A summaries.
# Files Changed
- `src/config/nav-config.ts`
- `src/hooks/use-breadcrumbs.tsx`
- `src/app/dashboard/employee-directory/page.tsx`
- `src/app/dashboard/employee-directory/[id]/page.tsx`
- `src/features/employee-directory/components/employee-directory-action-menu.tsx`
- `src/features/employee-directory/components/employee-directory-columns.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
- `src/features/employee-directory/components/employee-directory-listing.tsx`
- `src/features/employee-directory/components/employee-directory-detail-page.tsx`
- `src/features/employee-directory/server/employee-directory-data.ts`
- `docs/management-change-add-employee-directory-menu-review.md`
# Sidebar Changes
- Kept the existing `พนักงาน` submenu under `ข้อมูลหลัก`
- Added a new submenu:
- `รายชื่อพนักงานทั้งหมด`
- route: `/dashboard/employee-directory`
- Visibility is limited to HRD/Admin via existing role-based navigation rules
# New Routes
- `/dashboard/employee-directory`
- `/dashboard/employee-directory/[id]`
# Existing Employee Menu Kept
- The existing `พนักงาน` menu remains unchanged
- Existing route `/dashboard/users` remains unchanged
- Existing users page behavior is untouched
# Employee Directory Page
- Reuses the existing users data source for minimal-impact implementation
- Search supports the existing backend search across:
- employee code
- employee name
- email
- department
- position
- Table columns:
- รหัสพนักงาน
- ชื่อพนักงาน
- ฝ่าย
- ตำแหน่ง
- Action
- Action uses a compact three-dot menu with `ดูข้อมูลพนักงาน`
# Employee Detail Page
- Protected server-rendered detail page for HRD/Admin only
- Sections included:
1. ข้อมูลทั่วไปพนักงาน
2. ข้อมูลชั่วโมงอบรม
3. K / S / A Summary
4. รายการอบรม
- Training summary uses current-year targets and current-year approved/pending/rejected hours
- Training records list links back to the existing training record detail pages
# Permission Rules
- HRD/Admin:
- can see the new menu
- can access the directory list
- can open employee detail pages
- Employee:
- does not see the menu
- cannot access the routes directly because the pages use `requireHRDDashboardAccess()`
# Responsive Notes
- The directory table uses the existing scrollable table container
- The action column is compact and pinned-friendly
- Detail sections stack into single-column layouts on smaller screens
- Training records in the detail page use horizontal scrolling inside the section instead of expanding page width
# Manual Test Checklist
- [ ] Existing `พนักงาน` menu remains
- [ ] New `รายชื่อพนักงานทั้งหมด` menu appears under `ข้อมูลหลัก`
- [ ] HRD/Admin can open employee directory
- [ ] Employee cannot see employee directory menu
- [ ] Employee cannot access employee directory by URL
- [ ] Employee directory table shows employee code
- [ ] Employee directory table shows employee name
- [ ] Employee directory table shows department/faction
- [ ] Employee directory table shows position
- [ ] Action opens employee detail
- [ ] Detail page shows general employee data
- [ ] Detail page shows training hour summary
- [ ] Detail page shows K/S/A summary
- [ ] Detail page shows training records
- [ ] Mobile layout works
- [ ] Existing `พนักงาน` page still works
# Known Limitations
- The current schema does not have a separate dedicated `ฝ่าย` field, so this page displays the available department data in that slot.
- The summary cards use the current year for target and hour progress calculations.
- Certificate status in the training-record list is shown as file count with a link to the existing training record detail page.

View File

@@ -0,0 +1,67 @@
# Admin Sidebar Review
## 1. Summary
อัปเดต sidebar สำหรับ Admin/HRD ตาม feedback โดยซ่อนเมนู:
- `องค์กร`
- `ตรวจสอบข้อมูลหลัก`
การเปลี่ยนแปลงนี้เป็นการแก้เฉพาะ navigation config เท่านั้น ไม่ได้ลบ route, API, database table หรือ backend logic ที่เกี่ยวข้อง
## 2. Files Changed
- `src/config/nav-config.ts`
## 3. Navigation Changes
เมนูที่ถูกนำออกจาก sidebar ของ Admin/HRD:
- `/dashboard/organizers`
- `/dashboard/master-review`
เมนูอื่นของ HRD/Admin ยังคงอยู่ตามเดิม เช่น:
- แดชบอร์ด
- รอตรวจสอบ
- ประวัติการอบรม
- พนักงาน
- หลักสูตร
- นโยบายการอบรม
- นำเข้าพนักงาน
- ประกาศ
- การแจ้งเตือน
- รายงาน
## 4. Routes Kept
route และ backend ที่ยังคงอยู่:
- `/dashboard/organizers`
- `/dashboard/master-review`
- API และ service logic ที่เกี่ยวข้องกับ organizers
- API และ service logic ที่เกี่ยวข้องกับ master review
## 5. Permission Impact
- มีผลเฉพาะการแสดงผลเมนูใน sidebar
- ไม่ได้เปลี่ยน permission model
- ไม่ได้เปลี่ยน route protection
- Employee sidebar ไม่ได้รับผลกระทบ
- Super admin ยังใช้ route เดิมได้หากเข้าตรงตามสิทธิ์ที่ระบบมีอยู่
## 6. Manual Test Checklist
- [ ] HRD/Admin sidebar no longer shows `องค์กร`
- [ ] HRD/Admin sidebar no longer shows `ตรวจสอบข้อมูลหลัก`
- [ ] Other HRD menus still display correctly
- [ ] Employee menus are not affected
- [ ] Dashboard still works
- [ ] Employee Import still works
- [ ] Master Review backend/API is not deleted
- [ ] No route or API was removed
## 7. Known Limitations
- การเปลี่ยนครั้งนี้ซ่อนเมนูออกจาก sidebar เท่านั้น ถ้ามีลิงก์ภายในระบบหรือผู้ใช้เข้าผ่าน URL ตรง ระบบ route เดิมยังคงมีอยู่
- ถ้าภายหลังต้องการซ่อนจาก role อื่นเพิ่ม ควรทำผ่าน `src/config/nav-config.ts` ต่อใน pattern เดิม

View File

@@ -0,0 +1,58 @@
# Certificate Dialog Review
## Summary
ปรับปรุงหน้าต่างแสดงตัวอย่างเกียรติบัตรให้ปุ่มปิดอยู่ในตำแหน่งที่เหมาะสมภายใน dialog และทำให้การแสดงผลรูปภาพมีขนาดที่พอดีกับทั้ง desktop และ mobile โดยไม่กระทบพฤติกรรม preview/fallback และการเปิดไฟล์เดิม
## Files Changed
- `src/components/ui/dialog.tsx`
- `src/features/training-records/components/training-record-tables/training-record-image-preview.tsx`
## UI Changes
- เพิ่มความสามารถ `hideCloseButton` ให้ `DialogContent` เพื่อซ่อนปุ่มปิดแบบ default ในกรณีที่ต้องการจัด header เอง
- เพิ่ม header แบบมองเห็นได้ใน dialog preview
- ซ้าย: หัวข้อ `ตัวอย่างเกียรติบัตร`
- ขวา: ปุ่มปิดแบบ `DialogClose` + `Button`
- เพิ่มเส้นแบ่ง (`border-b`) ระหว่าง header และเนื้อหา preview
## Dialog Behavior
- ปุ่มปิดอยู่ภายในกรอบ dialog เสมอ
- ปุ่มปิดไม่ลอยทับรูปภาพ
- ปุ่มปิดใช้ `DialogClose asChild` ตาม pattern ของ shadcn/ui
- dialog ยังคงเปิดจาก thumbnail เดิม และยังรองรับ fallback placeholder เหมือนเดิม
## Image Sizing
- เปลี่ยนพื้นที่ preview หลักให้ใช้ `object-contain`
- จำกัดขนาดรูปด้วย `max-h-[70vh]`
- ให้รูปใช้ `w-full` ภายใน dialog โดยไม่ล้นกรอบ
- dialog ใช้ขนาด `w-[calc(100vw-2rem)]` และ `max-w-3xl` เพื่อให้พอดีกับหน้าจอ
## Mobile/Responsive Notes
- dialog ใช้ความกว้างแบบ responsive เพื่อไม่ชนขอบจอมือถือ
- header ใช้งานง่ายบนหน้าจอเล็ก เพราะปุ่มปิดอยู่ในแถวเดียวกับหัวข้อ
- เนื้อหาใช้ `overflow-y-auto` เพื่อรองรับกรณีความสูงไม่พอ
- placeholder non-image และ broken-image ยังแสดงได้เต็มพื้นที่โดยไม่แตก layout
## Manual Test Checklist
- Certificate preview dialog opens correctly.
- Close button stays inside dialog.
- Close button does not overlap image.
- Close button works on desktop.
- Close button works on mobile.
- Large landscape certificate fits inside dialog.
- Portrait certificate fits inside dialog.
- Image does not overflow screen.
- PDF or unsupported file shows certificate placeholder icon.
- Broken image shows certificate placeholder icon.
- Download/open certificate still works.
## Known Limitations
- งานนี้ปรับเฉพาะ dialog ของ certificate preview component ที่ใช้งานอยู่ในโมดูล training records
- ถ้าภายหลังมี preview dialog แบบใหม่ที่ไม่ใช้ component เดียวกัน จะต้องนำ pattern นี้ไปใช้เพิ่มแยกจุด

View File

@@ -0,0 +1,48 @@
# Management Change Certificate Empty State Review
## Summary
Updated certificate preview fallback states in training records so the UI never shows a broken image, empty cell, or `-` for missing attachments. The table and certificate manager now use a shared placeholder style with the same certificate icon treatment.
## Changes
- Added shared empty-state support to `CertificatePlaceholder`
- Updated training record table preview to show:
- image preview when previewable image exists
- certificate placeholder with file type for non-previewable or broken previews
- certificate placeholder with `ไม่มีไฟล์แนบ` when no file exists
- Updated certificate manager empty state to use the same placeholder card instead of plain text
## Files
- `src/features/training-records/components/certificate-preview-utils.tsx`
- `src/features/training-records/components/training-record-tables/training-record-image-preview.tsx`
- `src/features/training-records/components/training-record-certificate-manager.tsx`
## Behavior
Case 1: Previewable image exists
- shows image preview
Case 2: File exists but is not previewable, or image preview fails
- shows certificate icon
- shows `เกียรติบัตร`
- shows file type such as `PDF`, `DOC`, `XLS`, `ZIP`
Case 3: No file attached
- shows certificate icon
- shows `ไม่มีไฟล์แนบ`
## Manual Test Checklist
- [ ] Image certificate displays preview.
- [ ] PDF certificate shows certificate icon and PDF.
- [ ] DOCX certificate shows certificate icon and DOCX.
- [ ] Broken image shows certificate icon.
- [ ] No attachment shows certificate icon and `ไม่มีไฟล์แนบ`.
- [ ] No table cell displays `-`.
- [ ] Download/open still works when file exists.
- [ ] Mobile/table layout does not break.

View File

@@ -0,0 +1,78 @@
# Certificate Preview Review
## Summary
ปรับปรุงการแสดงตัวอย่างใบรับรองในโมดูล Training Records ให้รองรับ fallback ที่ชัดเจนขึ้น:
- ไฟล์รูปภาพ `JPG`, `JPEG`, `PNG`, `GIF`, `WEBP` แสดง preview ได้
- หากรูปเสีย, โหลดไม่ได้, URL ไม่ถูกต้อง, หรือเป็นไฟล์ที่ไม่ใช่รูปภาพ จะไม่แสดง broken image
- ใช้ placeholder แบบเดียวกันในตารางประวัติอบรม, หน้ารายละเอียด, และรายการเอกสารแนบ
- การเปิดไฟล์/ดาวน์โหลดไฟล์ยังทำงานเหมือนเดิม
## Files Changed
- `src/components/icons.tsx`
- `src/features/training-records/components/certificate-preview-utils.tsx`
- `src/features/training-records/components/training-record-certificate-manager.tsx`
- `src/features/training-records/components/training-record-tables/columns.tsx`
- `src/features/training-records/components/training-record-tables/training-record-image-preview.tsx`
- `src/features/training-records/server/training-record-data.ts`
## Preview Logic
1. ดึง certificate ล่าสุดของแต่ละ training record เหมือนเดิม
2. ถ้า `fileType` เป็นรูปภาพที่รองรับ จะส่ง preview URL กลับมาให้ UI
3. ถ้า `fileType` ไม่ใช่รูปภาพ จะไม่ส่ง preview URL แต่ยังส่งชนิดไฟล์กลับมาเพื่อใช้แสดง placeholder
4. ฝั่ง UI ตรวจสอบอีกชั้นว่า:
- ถ้าเป็นรูปและโหลดได้ ให้แสดงรูป
- ถ้าเป็นรูปแต่โหลดไม่ได้ ให้ fallback เป็น placeholder
- ถ้าไม่ใช่รูป ให้แสดง placeholder ทันที
5. ถ้าไม่มีไฟล์แนบเลย จะแสดง `-`
## Placeholder Design
Placeholder ใช้ไอคอน `Award` จากชุดไอคอนของโปรเจกต์ และมีองค์ประกอบดังนี้:
- ไอคอนใบรับรอง
- ข้อความ `เกียรติบัตร`
- ชนิดไฟล์แบบ optional เช่น `PDF`, `DOCX`, `ZIP`
ดีไซน์ใช้กรอบ `border-dashed`, พื้นหลัง muted, และจัดวางกึ่งกลางเพื่อให้ดูสอดคล้องกับ UI เดิมของระบบ
## Supported File Types
Preview รูปภาพ:
- `image/jpeg`
- `image/jpg`
- `image/png`
- `image/gif`
- `image/webp`
Fallback placeholder:
- `application/pdf`
- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`
- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
- `application/vnd.openxmlformats-officedocument.presentationml.presentation`
- `application/zip`
- `application/x-rar-compressed`
- และไฟล์ชนิดอื่นที่ไม่ใช่รูปภาพ
## Manual Test Checklist
- อัปโหลดไฟล์ `JPG` แล้วตรวจสอบว่า preview แสดงถูกต้อง
- อัปโหลดไฟล์ `PNG` แล้วตรวจสอบว่า preview แสดงถูกต้อง
- ตรวจสอบ record ที่มี `GIF` หรือ `WEBP` ว่า preview แสดงถูกต้อง
- ทำให้ URL รูปภาพไม่ถูกต้อง แล้วตรวจสอบว่าแสดง placeholder แทน broken image
- ตรวจสอบไฟล์ `PDF` ว่าแสดง placeholder พร้อมชนิดไฟล์
- ตรวจสอบไฟล์ที่ไม่ใช่รูปภาพอื่น เช่น `DOCX`, `XLSX`, `PPTX`, `ZIP`, `RAR` ว่าแสดง placeholder
- กดปุ่มเปิดไฟล์จากรายการเอกสารแนบ และตรวจสอบว่าเปิดไฟล์ได้
- ตรวจสอบหน้าตาราง Training Records
- ตรวจสอบหน้ารายละเอียด Training Record
- ตรวจสอบรายการเอกสารแนบใน Certificate Manager
## Known Limitations
- UI สำหรับอัปโหลดไฟล์ยังคงจำกัดชนิดไฟล์ตาม validation เดิมของระบบ; งานนี้โฟกัสที่ preview/fallback behavior เป็นหลัก
- Placeholder แสดงชนิดไฟล์จาก MIME type ที่ระบบมีอยู่ ถ้าแหล่งข้อมูลส่ง MIME type แปลกหรือไม่ครบ อาจแสดง label ได้ไม่สมบูรณ์

View File

@@ -0,0 +1,81 @@
# Course Selection Review
## Summary
ปรับฟอร์มบันทึกประวัติการอบรมของพนักงานจากช่องกรอกชื่อหลักสูตรแบบข้อความธรรมดา เป็น searchable course combobox ที่รองรับทั้งการเลือกจาก Course Master และการกรอกชื่อหลักสูตรเอง โดยยังคงบันทึก `courseName` เดิมไว้ และเพิ่มการผูก `courseId` เมื่อเลือกจากหลักสูตรกลาง
## Files Changed
- `src/app/api/courses/route.ts`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/features/training-records/api/types.ts`
- `src/features/training-records/api/service.ts`
- `src/features/training-records/api/queries.ts`
- `src/features/training-records/components/training-record-course-combobox.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/server/training-record-data.ts`
## UI Changes
- เปลี่ยน field หลักสูตรเป็น combobox แบบค้นหาได้
- แสดงรายการด้วยรูปแบบ `courseCode - courseName`
- เพิ่มตัวเลือก `อื่น ๆ / กรอกชื่อหลักสูตรเอง`
- เมื่อเลือก custom course จะแสดง input สำหรับกรอกชื่อหลักสูตรเอง
- layout ยังใช้งานได้บน mobile และ desktop
## Course Selection Behavior
- ผู้ใช้ค้นหาหลักสูตรจาก Course Master ได้ด้วยชื่อหลักสูตรหรือรหัสหลักสูตร
- แสดงเฉพาะหลักสูตรที่ active
- เมื่อเลือกหลักสูตรจาก Course Master:
- บันทึก `courseId`
- กำหนด `courseName` ตามชื่อหลักสูตรจาก master
- auto-fill `organizer` ถ้ามี
- auto-fill `hours` จาก `standardHours` ถ้ามี
## Custom Course Behavior
- ผู้ใช้สามารถเลือก `อื่น ๆ / กรอกชื่อหลักสูตรเอง`
- ในกรณีนี้:
- `courseId = null`
- ต้องกรอกชื่อหลักสูตรเอง
- ระบบยังบันทึก `courseName` ตามค่าที่กรอก
## Validation Rules
- ต้องเลือกหลักสูตรจาก master หรือกรอกชื่อหลักสูตรเอง
- ถ้าเลือก custom course จะต้องกรอกชื่อหลักสูตร
- `hours` ต้องมากกว่า 0
- `courseName` จะไม่ถูกปล่อยให้ว่างก่อน submit
## Database Behavior
- ถ้าเลือกจาก Course Master:
- บันทึก `courseId`
- บันทึก `courseName` จากข้อมูลหลักสูตรกลาง
- ถ้าเป็น custom course:
- บันทึก `courseId = null`
- บันทึก `courseName` จากข้อความที่ผู้ใช้กรอก
- ไม่มีการลบ field `courseName` เดิม
## Manual Test Checklist
- Employee can search course from Course Master.
- Employee can select course from Course Master.
- Selected course fills course name.
- Selected course fills provider if available.
- Selected course fills default hours if available.
- Employee can choose custom course.
- Custom course name is required.
- Saving master course stores courseId.
- Saving custom course stores courseId as null.
- HRD review still displays course name correctly.
- Duplicate warning still works.
- Mobile layout works.
## Known Limitations
- งานนี้ใช้ route `/api/courses` เดิมในการอ่าน course list และเปิดสิทธิ์อ่านให้ผู้ใช้ในองค์กร แต่ยังคงจำกัดการสร้างหลักสูตรไว้ที่ HRD
- ถ้าหลักสูตรเดิมของ record ถูกปิดใช้งานภายหลัง ระบบยังคงแสดงชื่อหลักสูตรเดิมได้ แต่ตัวเลือกค้นหาใหม่จะแสดงเฉพาะหลักสูตร active

View File

@@ -0,0 +1,142 @@
# Management Change Duration Minutes Review
## 1. Summary
This round starts the migration of training duration handling from decimal hours to total minutes with backward compatibility. The core `training-records` create/edit/review flow now accepts minute-based values, stores both minute and legacy decimal fields, and displays Thai duration text in the main training record UI.
## 2. Database Changes
Added new nullable minute-based fields:
- `training_records.submitted_minutes`
- `training_records.approved_minutes`
- `training_policies.total_target_minutes`
- `training_policies.k_target_minutes`
- `training_policies.s_target_minutes`
- `training_policies.a_target_minutes`
- `employee_training_targets.total_target_minutes`
- `employee_training_targets.k_target_minutes`
- `employee_training_targets.s_target_minutes`
- `employee_training_targets.a_target_minutes`
Migration file:
- `drizzle/0012_change_duration_minutes.sql`
## 3. Migration Strategy
The migration keeps legacy decimal columns and backfills new minute columns using:
- `ROUND(hours * 60)`
- `ROUND(approved_hours * 60)`
- `ROUND(total_hours * 60)`
- `ROUND(k_hours * 60)`
- `ROUND(s_hours * 60)`
- `ROUND(a_hours * 60)`
This allows old code paths to keep working while new UI/API paths move to minutes.
## 4. Duration Picker UI
Added:
- `src/components/ui/duration-picker.tsx`
Behavior:
- two dropdowns: `ชั่วโมง` and `นาที`
- hour range `00` to `30`
- minute step `00`, `15`, `30`, `45`
- no AM/PM
- no `Date` storage
- helper display in `HH:MM`
## 5. Storage Behavior
Updated the training record flow so that:
- form submit sends `submittedMinutes`
- review submit sends `approvedMinutes`
- route handlers write new minute fields
- route handlers still write decimal `hours` and `approved_hours` for compatibility
The compatibility helper is:
- `getDurationMinutes(minutesField, legacyHoursField)`
## 6. Display Formatting
Added shared helpers in:
- `src/features/training-records/utils/time-utils.ts`
Helpers:
- `formatDurationThai(minutes)`
- `formatDurationHHMM(minutes)`
- `decimalHoursToMinutes(hours)`
- `minutesToDecimalHours(minutes)`
- `getDurationMinutes(minutesField, legacyHoursField)`
Main updated displays:
- training record form
- training record review page
- training record table
- pending review table
## 7. Dashboard Impact
Not fully migrated yet.
Current state:
- the training record source data now exposes `submitted_minutes` and `approved_minutes`
- dashboard aggregate logic in `src/features/overview/server/overview-data.ts` still sums legacy decimal hour columns
Required next step:
- switch aggregate expressions to minute-first calculations with decimal fallback only when minute fields are null
## 8. Reports Impact
Not fully migrated yet.
Current state:
- training record API now exposes minute fields
- report dataset and exports still primarily rely on decimal hour aggregations
Required next step:
- update `src/features/reports/server/report-data.ts` and export formatters to sum/display minutes consistently
## 9. Backward Compatibility
Compatibility is preserved by:
- keeping old decimal DB columns
- still writing `hours` / `approved_hours`
- deriving minute display from minute columns first, then decimal fallback
This reduces breakage during staged migration.
## 10. Manual Test Checklist
- [ ] `00:15` saves `15` minutes.
- [ ] `01:45` saves `105` minutes.
- [ ] `02:00` saves `120` minutes.
- [ ] `30:00` saves `1800` minutes.
- [ ] `00:00` is blocked.
- [ ] More than `30:00` is blocked.
- [ ] Training history shows `1 ชั่วโมง 45 นาที`.
- [ ] Dashboard sums minutes correctly.
- [ ] Reports export minutes correctly.
- [ ] Legacy decimal hours still display correctly.
- [ ] No AM/PM appears anywhere.
## 11. Known Limitations
- Overview, reports, employee directory, notifications, and audit log text are not fully migrated to minute-first calculations yet.
- Training policy and employee target UI still use hour-based contracts even though DB minute columns are prepared.
- Export Excel/PDF paths were not updated in this round.

View File

@@ -0,0 +1,59 @@
# Management Change Import Button In Users Review
## 1. Summary
Moved `นำเข้าพนักงาน` out of the sidebar and exposed it as a page-header action on the Users page, next to the existing add-user action. The import route and protection rules remain unchanged.
## 2. Files Changed
- `src/config/nav-config.ts`
- `src/app/dashboard/users/page.tsx`
- `docs/management-change-import-button-in-users-review.md`
## 3. Sidebar Changes
- Removed the root-level sidebar menu item:
- `นำเข้าพนักงาน`
- Did not re-add it anywhere else in the sidebar.
- The `ข้อมูลหลัก` group remains:
- พนักงาน
- หลักสูตร
- นโยบาย
- ประกาศ
## 4. Users Page Changes
- Added a new page-header button on the Users page:
- `นำเข้าพนักงาน`
- Button links to:
- `/dashboard/import-employees`
- Kept the existing add-user action in place.
- Header actions now render side by side on desktop and stack on mobile.
## 5. Permission Rules
- The Users page is already protected with HRD/Admin access, so the import button is only visible to HRD/Admin users on that page.
- Employee users do not see the button because they do not have access to the Users page.
- Direct access to `/dashboard/import-employees` remains protected by existing route/page guards.
## 6. Responsive Notes
- Desktop: action buttons align on the right in the page header.
- Mobile: buttons stack vertically and use full width.
- No new overflow behavior was introduced.
## 7. Manual Test Checklist
- [x] Sidebar no longer shows `นำเข้าพนักงาน`.
- [x] Sidebar still shows `พนักงาน`.
- [x] Users page shows `นำเข้าพนักงาน` button for HRD/Admin.
- [x] Button links to `/dashboard/import-employees`.
- [x] Add User button still works.
- [x] Employee user does not see import button through existing page protection.
- [x] Direct `/dashboard/import-employees` remains HRD/Admin only.
- [x] Mobile layout does not overflow structurally.
## 8. Known Limitations
- Visual QA in-browser is still recommended to confirm final spacing and button wrapping on smaller devices.
- The button is placed on the Users page, which is the active employee-management route in the current app. The legacy `/dashboard/employees` route remains untouched.

View File

@@ -0,0 +1,175 @@
# Management Change Import Employee KSA Targets Review
## 1. Summary
เพิ่มการนำเข้าชั่วโมงเป้าหมายรายบุคคล K/S/A ผ่านหน้า Import Employees โดยยังคงพฤติกรรมเดิมของการนำเข้าพนักงานไว้ครบถ้วน และเพิ่ม fallback จาก `Training Policy` เมื่อแถวใดไม่ระบุเป้าหมายรายบุคคล
ลำดับการ resolve เป้าหมายที่ระบบใช้หลังการเปลี่ยนแปลง:
1. `employee_training_targets`
2. `training_policies`
3. ค่าเริ่มต้น 0 ชั่วโมง
## 2. Files Changed
- `src/db/schema.ts`
- `src/constants/thai-labels.ts`
- `src/features/training-policy/server/employee-training-target-data.ts`
- `src/features/import-employees/api/types.ts`
- `src/app/api/import-employees/route.ts`
- `src/app/api/import-employees/template/route.ts`
- `src/features/import-employees/components/employee-import-page.tsx`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/audit-logs/server/audit-log-data.ts`
- `src/features/overview/server/overview-data.ts`
- `src/features/reports/api/types.ts`
- `src/features/reports/server/report-data.ts`
- `src/features/reports/components/reports-page-content.tsx`
## 3. Database Changes
เพิ่มตาราง `employee_training_targets`
- `id`
- `organization_id`
- `user_id`
- `year`
- `total_hours`
- `k_hours`
- `s_hours`
- `a_hours`
- `created_by`
- `updated_by`
- `created_at`
- `updated_at`
Unique constraint:
- `organization_id + user_id + year`
เหตุผลที่ใช้ `user_id`:
- ระบบ runtime ปัจจุบันใช้ `users` เป็น source of truth
- dashboard, reports, training records อ้างอิง `training_records.user_id`
## 4. Excel Template Changes
เพิ่มคอลัมน์ท้าย template:
- `ชั่วโมงรวม`
- `ชั่วโมง K`
- `ชั่วโมง S`
- `ชั่วโมง A`
Template download ตัวอย่างแถวข้อมูลถูกอัปเดตให้มีคอลัมน์ใหม่ครบ
## 5. Validation Rules
กติกา validation ที่เพิ่ม:
- ถ้าไม่กรอกคอลัมน์ target ทั้ง 4 ช่อง จะ import พนักงานตามปกติ
- ถ้ากรอก target อย่างน้อย 1 ช่อง ต้องกรอกให้ครบทั้ง 4 ช่อง
- ต้องเป็นตัวเลข
- ต้องมากกว่าหรือเท่ากับ 0
- `ชั่วโมงรวม` ต้องมากกว่า 0
- `K + S + A` ต้องเท่ากับ `ชั่วโมงรวม`
- ปีที่ใช้บันทึก target จะใช้ปีปัจจุบัน หรือปีที่ส่งมาจาก form ถ้ามี
- ถ้าส่งปีแบบ พ.ศ. ระบบจะแปลงเป็น ค.ศ. ก่อนจัดเก็บ
ข้อความ error หลักที่รองรับ:
- `กรุณาระบุชั่วโมงรวม`
- `กรุณาระบุชั่วโมง K`
- `กรุณาระบุชั่วโมง S`
- `กรุณาระบุชั่วโมง A`
- `ชั่วโมงต้องเป็นตัวเลข`
- `ชั่วโมงต้องเป็นตัวเลขที่มากกว่าหรือเท่ากับ 0`
- `ชั่วโมงรวมต้องมากกว่า 0`
- `ผลรวมของชั่วโมง K/S/A ต้องเท่ากับชั่วโมงรวม`
## 6. Import Behavior
ต่อ 1 แถวที่ผ่าน validation:
1. upsert `users` และ `memberships` ตาม behavior เดิม
2. ถ้ามี target รายบุคคล จะ upsert `employee_training_targets`
3. ถ้าไม่มี target รายบุคคล ระบบจะถือว่าแถวนั้นใช้ fallback จาก `Training Policy`
Import summary ที่เพิ่ม:
- จำนวนเป้าหมาย K/S/A ที่นำเข้า
- จำนวนรายการที่ใช้ `Training Policy`
## 7. Dashboard Impact
Employee dashboard:
- ใช้ target รายบุคคลก่อน
- ถ้าไม่พบ target รายบุคคล จะ fallback ไป `Training Policy`
- K/S/A progress และ completion cards จะอิง target ที่ resolve แล้ว
HRD dashboard:
- การคำนวณ `ผ่านเป้าหมาย` และ `ยังไม่ผ่านเป้าหมาย` เปลี่ยนเป็นใช้ target รายคน
- ถ้าไม่พบ target รายคน จะ fallback ไป `Training Policy`
## 8. Reports Impact
Training Matrix:
- เพิ่มข้อมูล `approvedHours`
- เพิ่มข้อมูล `targetHours`
- เพิ่ม `targetSource`
- เพิ่ม `targetStatus`
รายงานหน้า UI จะแสดง:
- ชั่วโมงอนุมัติ
- เป้าหมายชั่วโมง
- แหล่งที่มาของเป้าหมาย
- สถานะผ่าน/ยังไม่ผ่านเป้าหมาย
## 9. Audit Events
เพิ่ม audit actions:
- `EMPLOYEE_TARGET_CREATE`
- `EMPLOYEE_TARGET_UPDATE`
ข้อมูลที่บันทึก:
- `employeeCode`
- `year`
- ค่า target เดิม
- ค่า target ใหม่
## 10. Permission Rules
- ใช้ `requireHRD()` เหมือนเดิม
- เฉพาะ HRD/Admin ที่เข้าถึง import route ได้
- พนักงานไม่มี route สำหรับแก้ target รายบุคคลโดยตรง
## 11. Manual Test Checklist
- [ ] Download template includes K/S/A target columns.
- [ ] Import employee without K/S/A target works.
- [ ] Import employee with valid target works.
- [ ] Import creates employee-specific target.
- [ ] Re-import updates existing employee target.
- [ ] Import blocks missing partial target fields.
- [ ] Import blocks non-numeric target fields.
- [ ] Import blocks K+S+A not equal total.
- [ ] Import summary shows target counts.
- [ ] Employee dashboard uses employee target.
- [ ] Employee dashboard falls back to Training Policy when no target exists.
- [ ] HRD dashboard pass/fail uses employee target.
- [ ] Training Matrix uses employee target.
- [ ] Audit log records target create/update.
- [ ] Employee cannot edit target directly.
- [ ] Mobile import page remains responsive.
## 12. Known Limitations
- Import UI ปัจจุบันยังไม่มี year picker แยก จึงใช้ปีปัจจุบันเป็นค่า default
- Export header ของรายงานยังควรทดสอบกับข้อมูลจริงอีกครั้งหลัง migrate/seed
- การ import ต่อแถวไม่ได้ห่อ transaction แยกทั้งแถว จึงเป็น partial success ได้ตาม behavior เดิมของระบบ

View File

@@ -0,0 +1,54 @@
# Summary
Updated the Pending Review table action UI from a full-width `ตรวจสอบ` button to a compact three-dot action menu. The review workflow, routes, and backend behavior remain unchanged.
# Files Changed
- `src/features/training-records/components/pending-review-action-menu.tsx`
- `src/features/training-records/components/pending-review-columns.tsx`
- `docs/management-change-pending-review-action-menu-review.md`
# UI Changes
- Replaced the full text button in the Pending Review action column with a compact icon button.
- Used the existing `DropdownMenu` component and the existing icon system.
- Kept the action column compact with `w-[60px] min-w-[60px]`.
- Right-aligned the action trigger so it stays table-friendly.
# Action Menu Behavior
- Trigger button uses:
- `variant="ghost"`
- `size="icon"`
- `Icons.ellipsis`
- `aria-label="เมนูการดำเนินการ"`
- Dropdown items:
1. `ตรวจสอบ`
- navigates to `/dashboard/training-records/[id]/review`
- keeps the same behavior as the previous full button
2. `ดูรายละเอียด`
- navigates to `/dashboard/training-records/[id]`
# Responsive Notes
- The action column is now narrower, which reduces table width pressure on smaller screens.
- The dropdown content uses right alignment and a viewport-safe max width.
- Horizontal table scrolling behavior remains controlled by the existing table container.
- No route, data, or workflow changes were introduced.
# Manual Test Checklist
- [ ] Pending Review table no longer shows full `ตรวจสอบ` button.
- [ ] Three-dot button appears in action column.
- [ ] Clicking three-dot button opens action menu.
- [ ] `ตรวจสอบ` menu item opens existing review page.
- [ ] Review workflow still works.
- [ ] Table width is reduced.
- [ ] Mobile layout works.
- [ ] Dropdown does not overflow viewport.
# Known Limitations
- This change only updates the Pending Review table action UI.
- Other training record tables keep their existing action patterns.
- Final mobile behavior should still be verified in-browser because table overflow depends on real viewport width and row content.

View File

@@ -0,0 +1,87 @@
# Management Change Sidebar Master Data Group Review
## 1. Summary
Refactored the HRD/Admin sidebar so employee-related management links are grouped under a single collapsible parent menu named `ข้อมูลหลัก`, while keeping all existing routes, APIs, and role-based visibility intact.
## 2. Files Changed
- `src/config/nav-config.ts`
- `src/components/layout/app-sidebar.tsx`
- `docs/management-change-sidebar-master-data-group-review.md`
## 3. Navigation Before
HRD/Admin root-level sidebar included these separate items:
- พนักงาน
- หลักสูตร
- นโยบายการอบรม
- ประกาศ
These were shown alongside the rest of the main sidebar items.
## 4. Navigation After
HRD/Admin now sees a parent menu:
- ข้อมูลหลัก
Inside that menu:
- พนักงาน -> `/dashboard/users`
- หลักสูตร -> `/dashboard/courses`
- นโยบาย -> `/dashboard/training-policy`
- ประกาศ -> `/dashboard/announcements`
Other root-level HRD/Admin menus remain unchanged:
- แดชบอร์ด
- รอตรวจสอบ
- ประวัติการอบรม
- นำเข้าพนักงาน
- รายงาน
- บันทึกการใช้งาน
## 5. Role Impact
- HRD/Admin keeps access to the same management routes as before.
- Employee sidebar is unchanged because the new parent menu inherits the existing HRD-only access rule.
- Super Admin keeps the same visibility plus any existing privileged items such as Audit Log.
## 6. Active Route Behavior
- Parent menu `ข้อมูลหลัก` appears active when any child route is active.
- Child submenu item highlights correctly for:
- exact route matches
- nested detail pages under the same route prefix
- Root items now also use prefix-aware active matching, which improves sidebar highlighting for detail pages.
## 7. Mobile Sidebar Behavior
- Nested menu continues to use the existing collapsible sidebar pattern.
- Mobile drawer still closes after selecting a submenu item because the existing `handleNavigate` behavior is preserved.
- Thai labels continue to use the current truncation behavior from shared sidebar components.
## 8. Manual Test Checklist
- [x] HRD/Admin sees `ข้อมูลหลัก` parent menu.
- [x] `ข้อมูลหลัก` contains พนักงาน.
- [x] `ข้อมูลหลัก` contains หลักสูตร.
- [x] `ข้อมูลหลัก` contains นโยบาย.
- [x] `ข้อมูลหลัก` contains ประกาศ.
- [x] Root-level พนักงาน menu is removed.
- [x] Root-level หลักสูตร menu is removed.
- [x] Root-level นโยบาย menu is removed.
- [x] Root-level ประกาศ menu is removed.
- [x] Other HRD/Admin menus still work structurally.
- [x] Employee sidebar is unchanged by access rules.
- [x] Active submenu is highlighted.
- [x] Parent menu opens when child route is active.
- [x] Mobile sidebar nested menu keeps existing behavior.
- [x] No route or API was deleted.
## 9. Known Limitations
- Visual QA in the browser is still recommended to confirm final spacing and open-state behavior on both desktop and mobile.
- The parent menu uses the existing shared collapsible sidebar interaction, so any future changes to sidebar primitives will affect this group as well.

View File

@@ -0,0 +1,92 @@
# Training Course Dropdown Review
## Summary
ปรับฟอร์ม Create/Edit Training Record ให้แสดงฟิลด์หลักสูตรเป็น dropdown ตามค่าเริ่มต้น และแสดงช่องกรอกชื่อหลักสูตรเองเฉพาะเมื่อผู้ใช้เลือก `อื่น ๆ` โดยยังคงบันทึก `courseName` ทุกครั้ง และบันทึก `courseId` เฉพาะกรณีเลือกจาก Course Master
## Files Changed
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-course-combobox.tsx`
- `src/features/training-records/schemas/training-record.ts`
## UI Changes
- ฟิลด์ `ชื่อหลักสูตร *` แสดงเป็น dropdown/select แบบเต็มความกว้าง
- รายการหลักสูตรแสดงจาก Active Course Master
- ตัวเลือกสุดท้ายของ dropdown คือ `อื่น ๆ`
- เมื่อเลือก `อื่น ๆ` จะเปลี่ยนเป็น text input แบบเต็มความกว้าง
## Course Dropdown Behavior
- dropdown แสดงหลักสูตร active เท่านั้น
- รูปแบบการแสดงผล:
- ถ้ามี `courseCode` ใช้ `courseCode - courseName`
- ถ้าไม่มี `courseCode` ใช้ `courseName`
- เมื่อเลือกหลักสูตรจาก master:
- ตั้งค่า `courseId`
- ตั้งค่า `courseName`
- auto-fill `organizer` ถ้ามี
- auto-fill `hours` จาก `standard_hours` ถ้ามี
## Custom Course Behavior
- เมื่อเลือก `อื่น ๆ`
- ซ่อน dropdown
- แสดง input `ชื่อหลักสูตรอื่น ๆ *`
- ตั้งค่า `courseId = null`
- ใช้ค่าจาก input ไปบันทึกเป็น `courseName`
- มีปุ่ม `เปลี่ยนกลับไปเลือกจากหลักสูตร`
- ล้างชื่อหลักสูตร custom
- กลับไปแสดง dropdown
- เคลียร์ `courseId` และ `courseName`
## Edit Mode Behavior
- ถ้า record เดิมมี `courseId`
- เปิดมาด้วย dropdown ที่เลือกหลักสูตรเดิมไว้
- ถ้า record เดิมมี `courseId = null` และมี `courseName`
- เปิดมาด้วย text input ของ custom course
- แสดงชื่อหลักสูตรเดิมใน input
## Validation Rules
- ถ้าไม่ได้เลือก `อื่น ๆ`:
- ต้องเลือกหลักสูตรจาก dropdown
- แสดงข้อความ `กรุณาเลือกหลักสูตร`
- ถ้าเลือก `อื่น ๆ`:
- ต้องกรอกชื่อหลักสูตร
- แสดงข้อความ `กรุณากรอกชื่อหลักสูตร`
- ชั่วโมงอบรมต้องมากกว่า 0
- แสดงข้อความ `กรุณาระบุจำนวนชั่วโมงอบรม`
## Database Behavior
- บันทึก `courseName` ทุกครั้ง
- ถ้าเลือกจาก Course Master:
- บันทึก `courseId`
- ถ้าเลือก `อื่น ๆ`:
- บันทึก `courseId = null`
- ไม่มีการลบ field `courseName`
## Manual Test Checklist
- Create form shows course dropdown by default.
- Active Course Master records are shown in dropdown.
- "อื่น ๆ" appears as the last option.
- Selecting Course Master saves courseId and courseName.
- Selecting Course Master auto-fills provider if available.
- Selecting Course Master auto-fills hours if available.
- Selecting "อื่น ๆ" changes dropdown to text input.
- Custom course name is required when "อื่น ๆ" is selected.
- "เปลี่ยนกลับไปเลือกจากหลักสูตร" works.
- Edit existing master course shows dropdown selected.
- Edit existing custom course shows text input.
- HRD review page still shows course name correctly.
- Reports still show course name correctly.
- Mobile layout works.
## Known Limitations
- งานนี้ยังใช้ query course list เดิมและดึงมาชุดเดียวสำหรับ dropdown จึงยังไม่ได้เพิ่ม pagination หรือ server-side search
- หากมีหลักสูตรเดิมที่ถูกปิดใช้งานไปแล้ว แต่ record อ้างอิงอยู่ ระบบจะยังแสดงค่าที่เคยบันทึกได้ใน edit mode ผ่าน option ที่เติมเข้ามาชั่วคราว

View File

@@ -0,0 +1,94 @@
# Training Hours TimePicker Review
## 1. Summary
เปลี่ยนช่อง `submitted hours` ในฟอร์ม Create/Edit Training Record จาก `number input` เป็น `TimePicker-style input` โดยใช้ `Input type="time"` ที่มีอยู่แล้วในระบบ เพื่อหลีกเลี่ยงการเพิ่มโครงสร้างใหม่และลดผลกระทบกับโมดูลเดิม
ค่าที่ผู้ใช้เลือกใน UI จะถูกแปลงเป็น `decimal hours` ก่อน submit และยังคงบันทึกลงฐานข้อมูลในฟิลด์ `hours` แบบเดิม ไม่มีการแก้ schema หรือ API contract
## 2. Files Changed
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/utils/time-utils.ts`
## 3. UI Changes
- เปลี่ยนช่องกรอกชั่วโมงอบรมเป็นตัวเลือกเวลาแบบ `HH:mm`
- เปลี่ยน label เป็น `จำนวนชั่วโมงอบรม *`
- เพิ่ม helper text:
- `เลือกระยะเวลาอบรม เช่น 01:30 = 1.5 ชั่วโมง`
- เพิ่มข้อความสรุปค่าที่คำนวณแล้วใต้ input:
- `รวม X ชั่วโมง`
## 4. TimePicker Behavior
- ใช้ `Input type="time"` เพื่อให้ได้พฤติกรรม TimePicker ตาม browser/device มาตรฐาน
- กำหนด `step={900}` เพื่อรองรับการเลือกทีละ 15 นาที
- ตัวอย่าง:
- `00:30 -> 0.5`
- `01:00 -> 1`
- `01:30 -> 1.5`
- `02:15 -> 2.25`
- `07:30 -> 7.5`
## 5. Decimal Conversion Logic
เพิ่ม helper ที่ `src/features/training-records/utils/time-utils.ts`
- `decimalHoursToDate(hours: number): Date`
- `dateToDecimalHours(date: Date): number`
- `timeInputValueToDate(value: string): Date | undefined`
- `dateToTimeInputValue(date?: Date): string`
- `formatDecimalHours(hours?: number): string`
หลักการ:
- ใช้ base date คงที่ `1970-01-01`
- UI ใช้ `Date` ชั่วคราวเฉพาะในฟอร์ม
- ก่อน submit แปลงเป็น `hours + minutes / 60`
- ตอน edit แปลงค่าจาก decimal กลับเป็น `HH:mm`
## 6. Validation Rules
ปรับ validation ให้ชัดเจนทั้งใน schema และ field-level validator
- required
- มากกว่า `0`
- ไม่เกิน `24`
ข้อความ validation:
- `กรุณาระบุจำนวนชั่วโมงอบรม`
- `จำนวนชั่วโมงอบรมต้องมากกว่า 0`
- `จำนวนชั่วโมงอบรมต้องไม่เกิน 24 ชั่วโมง`
## 7. Edit Mode Behavior
- ถ้าเปิดฟอร์มแก้ไขและมีค่าเดิม เช่น `1.5`
- ระบบจะแปลงกลับเป็น `01:30` ในช่องเวลาอัตโนมัติ
- ถ้าเลือกหลักสูตรที่มี `standard_hours` ระบบจะเติมค่าเวลาให้ฟอร์มอัตโนมัติด้วย
## 8. Dashboard/Report Impact
ไม่มีผลกระทบกับ dashboard, review flow หรือ report calculation เพราะระบบ backend ยังรับและเก็บ `hours` เป็นเลข decimal เหมือนเดิม
## 9. Manual Test Checklist
- [ ] Create form shows TimePicker for training hours
- [ ] Selecting `01:30` saves `1.5` hours
- [ ] Selecting `02:00` saves `2` hours
- [ ] Selecting `02:15` saves `2.25` hours
- [ ] Empty value is blocked
- [ ] `00:00` is blocked
- [ ] Value above `24` hours is blocked
- [ ] Edit form converts `1.5` back to `01:30`
- [ ] Dashboard still calculates approved hours correctly
- [ ] Reports still export decimal hours correctly
- [ ] Mobile layout works
## 10. Known Limitations
- Native `time` input แสดงผลต่างกันเล็กน้อยตาม browser และ OS
- `placeholder` ไม่ใช่พฤติกรรมหลักของ `input[type="time"]` ในหลาย browser จึงใช้ helper text ใต้ช่องแทน
- เวลาที่ใช้ในฟอร์มเป็น UI-only state และไม่ได้ถูกเก็บเป็น `Date` ลงฐานข้อมูล

View File

@@ -0,0 +1,80 @@
# Training Type Options Review
## Summary
ปรับตัวเลือก `ประเภทการอบรม` ในโมดูล Training Records ให้เป็นรายการใหม่ 4 ตัวเลือกตามที่อนุมัติ และเพิ่มชั้น backward compatibility เพื่อรองรับข้อมูลเก่าในฐานข้อมูลโดยไม่ต้อง migrate enum เดิม
## Files Changed
- `src/constants/thai-labels.ts`
- `src/features/training-records/api/types.ts`
- `src/features/training-records/constants/training-record-options.ts`
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/server/training-record-data.ts`
## Training Type Options Updated
ตัวเลือกใหม่ที่แสดงใน UI:
- `INTERNAL_TRAINING` -> `อบรมภายใน (Internal Training)`
- `EXTERNAL_TRAINING` -> `อบรมภายนอก (Public Training)`
- `ONLINE_COURSE` -> `e-Learning / Online Course`
- `OJT` -> `On the Job Training (OJT)`
## Backward Compatibility Mapping
ค่าจากฐานข้อมูลเดิมถูก map อย่างปลอดภัยดังนี้:
- `internal` -> `INTERNAL_TRAINING`
- `external` -> `EXTERNAL_TRAINING`
- `online` -> `ONLINE_COURSE`
- `onsite` -> `EXTERNAL_TRAINING`
ค่าจาก UI ใหม่จะถูก map กลับก่อนบันทึกลง DB:
- `INTERNAL_TRAINING` -> `internal`
- `EXTERNAL_TRAINING` -> `external`
- `ONLINE_COURSE` -> `online`
- `OJT` -> `onsite`
## Validation Rules
- ฟอร์ม Create/Edit อนุญาตเฉพาะ 4 ค่าใหม่เท่านั้น
- ถ้าไม่ได้เลือกประเภทการอบรม จะแสดงข้อความ:
- `กรุณาเลือกประเภทการอบรม`
- เมื่อแก้ไข record เก่า ระบบจะ normalize ค่าเก่าเป็นค่าใหม่ก่อนแสดงในฟอร์ม
## UI Changes
- dropdown ในหน้า Create Training Record แสดงเฉพาะ 4 ตัวเลือกใหม่
- dropdown ในหน้า Edit Training Record แสดงเฉพาะ 4 ตัวเลือกใหม่
- label ในหน้ารายละเอียด ตาราง และหน้า review ใช้ข้อความใหม่
- filter options ของประเภทการอบรมใช้ข้อความและค่าใหม่
## Report/Filter Impact
- ตาราง Training Records แสดง label ใหม่
- HRD Review page แสดง label ใหม่
- Report export ที่ใช้ `getTrainingTypeLabel(...)` แสดงข้อความใหม่อัตโนมัติ
- filter ประเภทการอบรมสามารถส่งค่าชุดใหม่ได้ และ server จะ map กลับไป query ค่าเก่าใน DB
## Manual Test Checklist
- Create form shows only 4 new training type options.
- Edit form shows only 4 new training type options.
- Training detail displays Thai/English label correctly.
- HRD review displays training type correctly.
- Training table displays training type correctly.
- Reports display training type correctly.
- Old INTERNAL value still displays safely.
- Old EXTERNAL value still displays safely.
- Old ONLINE value maps to e-Learning / Online Course.
- Old ONSITE value maps safely.
- Validation blocks empty training type.
- Mobile dropdown works.
## Known Limitations
- ฐานข้อมูลยังใช้ enum เก่าอยู่ (`online/onsite/internal/external`) เพื่อหลีกเลี่ยงการ migrate schema ในรอบนี้
- ค่าประวัติเดิม `onsite` ถูก map ไปแสดงเป็น `อบรมภายนอก (Public Training)` ตาม requirement นี้; ถ้าภายหลัง business ต้องการให้ `onsite` ไปที่ `OJT` จะต้องเปลี่ยน mapping rule เพิ่ม

View File

@@ -0,0 +1,63 @@
# User Management IT Admin Only Review
## 1. Summary
Updated User Management access so the `พนักงาน` menu and related `/dashboard/users` + `/api/users*` endpoints are available only to IT admin access (`IT` business role and `super_admin` via existing role mapping). HRD/Admin keeps access to `รายชื่อพนักงานทั้งหมด` and the rest of the master-data menu.
## 2. Files Changed
- `src/config/nav-config.ts`
- `src/types/index.ts`
- `src/hooks/use-nav.ts`
- `src/lib/auth/roles.ts`
- `src/lib/auth/session.ts`
- `src/lib/auth/page-guards.ts`
- `src/app/dashboard/users/page.tsx`
- `src/app/api/users/route.ts`
- `src/app/api/users/[id]/route.ts`
- `src/features/users/server/user-data.ts`
- `src/features/users/info-content.ts`
## 3. Navigation Changes
- Added support for `businessRoles` in nav access rules.
- Changed `พนักงาน` under `ข้อมูลหลัก` to visible only for `IT`.
- Kept `รายชื่อพนักงานทั้งหมด`, `หลักสูตร`, `นโยบาย`, and `ประกาศ` visible for both `HRD` and `IT`.
- Updated nav filtering so parent items with no visible children are hidden instead of leaving a stray direct link.
## 4. Permission Rules
- Added `requireUserManagementAccess()` as the shared access gate for user-management features.
- Changed `canManageUsers()` to allow only IT user-management access instead of any organizer admin.
- Existing `super_admin` behavior is preserved because the current business-role mapping resolves `super_admin` to `IT`.
## 5. Route Protection
- `/dashboard/users` now uses `requireUserManagementDashboardAccess()`.
- Unauthorized users are redirected using the existing dashboard guard behavior.
## 6. API Protection
- `/api/users`
- `/api/users/[id]`
Both routes now require `requireUserManagementAccess()` before any list, options, create, update, or deactivate action.
## 7. Manual Test Checklist
- Admin IT sees `พนักงาน` menu.
- HRD/Admin does not see `พนักงาน` menu.
- Employee does not see `พนักงาน` menu.
- HRD/Admin still sees `รายชื่อพนักงานทั้งหมด`.
- Admin IT can open `/dashboard/users`.
- HRD/Admin cannot open `/dashboard/users` by direct URL.
- Employee cannot open `/dashboard/users` by direct URL.
- User management APIs reject HRD/Admin.
- Employee Directory is not affected.
- Other menus still work.
## 8. Known Limitations
- This change assumes the current role mapping remains: `super_admin` => `IT`, organizer admin => `HRD`, member => `EMPLOYEE`.
- There is no dedicated `admin_it` database field yet; access still relies on the existing derived business-role model.
- Only the current `/dashboard/users` page exists in App Router right now, so no additional `/dashboard/users/[id]` page update was needed.

View File

@@ -0,0 +1,50 @@
# Summary
ปรับ User Dropdown Menu ตาม feedback โดยเพิ่มการแสดงรหัสพนักงานในเมนูผู้ใช้ และลบเมนู Profile ออก พร้อมคงการใช้งาน Dashboard และ Log out ไว้ตามเดิม
# Files Changed
- `src/components/layout/user-nav.tsx`
- `src/auth.ts`
- `src/types/next-auth.d.ts`
# Session Changes
- เพิ่ม `employeeCode` ในข้อมูลที่ return จาก Auth.js credentials authorize callback
- เพิ่ม `session.user.employeeCode` ใน session callback
- ขยาย type ของ NextAuth session/user ให้รองรับ `employeeCode: string | null`
# UI Changes
- User dropdown แสดงข้อมูลดังนี้:
- ชื่อผู้ใช้
- `รหัสพนักงาน: <employeeCode>`
- อีเมล
- หากไม่มีรหัสพนักงาน จะแสดง `รหัสพนักงาน: -`
- ลบเมนู `Profile` ออกจาก dropdown
- คงเมนู `แดชบอร์ด` และ `ออกจากระบบ`
- ปรับข้อความใน dropdown เป็นภาษาไทย
# Permission/Security Notes
- ใช้เฉพาะ `employeeCode` จาก user/session ที่มีอยู่แล้วในระบบ
- ไม่มีการ expose `passwordHash` หรือ field อ่อนไหวอื่นเพิ่ม
- การเปลี่ยนแปลงนี้เป็น presentation-level change บนข้อมูลที่ session ใช้งานอยู่แล้ว
# Manual Test Checklist
- User dropdown shows name.
- User dropdown shows employee code.
- User dropdown shows email.
- Missing employee code displays "-".
- Profile menu is removed.
- Dashboard menu still works.
- Log out still works.
- No password hash or sensitive data is exposed.
- Works for Employee.
- Works for HRD.
# Known Limitations
- หากข้อมูล `employeeCode` ในตาราง `users` เป็นค่าว่าง ระบบจะแสดง `-` ตามที่กำหนด
- การเปลี่ยนแปลงนี้ไม่แก้ไขข้อมูลต้นทางของผู้ใช้ในฐานข้อมูล มีผลเฉพาะการแสดงผลใน dropdown

View File

@@ -0,0 +1,64 @@
# Management Change Users Employee Code Review
## 1. Summary
Enhanced the Users Management table to include an Employee Code column, aligned the single search box with server-side employee-code/name/email searching, and adjusted mobile visibility so the employee code remains visible while less important content is reduced on smaller screens.
## 2. Files Changed
- `src/types/data-table.ts`
- `src/components/ui/table/data-table.tsx`
- `src/features/users/server/user-data.ts`
- `src/features/users/components/users-table/index.tsx`
- `src/features/users/components/user-listing.tsx`
- `src/features/users/components/users-table/columns.tsx`
- `docs/management-change-users-employee-code-review.md`
## 3. Table Changes
- Added `employee_code` as the first visible data column.
- Display label is `รหัสพนักงาน`.
- Missing employee codes render as `-`.
- Removed the organizer column from this table so the primary user-management fields match the requested order more closely.
- Kept row actions at the far right.
## 4. Search Changes
- Updated the single search box placeholder to `ค้นหาด้วยรหัสพนักงาน ชื่อ หรืออีเมล...`
- Search now runs server-side against:
- employee code
- name
- email
- Removed unrelated search matching on username, department, and position for this listing.
- Aligned the search query parameter between the table toolbar and the server-prefetch path.
## 5. Sorting Changes
- Sorting remains enabled for:
- `employee_code`
- `full_name`
- Server-side sort mapping was narrowed to the supported fields for this change.
## 6. Responsive Changes
- Employee Code remains visible on mobile.
- Email is hidden inside the name cell on smaller breakpoints.
- Position and Role columns are hidden on mobile via responsive table-cell classes.
- The existing responsive table container behavior remains intact, so the table still scrolls inside its own area without pushing the full page width.
## 7. Manual Test Checklist
- [x] Employee Code column appears.
- [x] Employee Code sorting works.
- [x] Search by Employee Code works.
- [x] Search by Name works.
- [x] Search by Email works.
- [x] Placeholder updated.
- [x] Missing employee code displays `-`.
- [x] Mobile layout works structurally.
- [x] Table does not overflow.
## 8. Known Limitations
- Visual breakpoint QA should still be checked in-browser for the final mobile/tablet polish.
- The table still includes the action column on the right because it remains necessary for user management operations.

221
docs/nav-rbac.md Normal file
View File

@@ -0,0 +1,221 @@
# Navigation and RBAC
## Overview
This repository uses an Auth.js-based access model with server-side authorization as the real security boundary.
Navigation filtering exists for usability only. It helps show the right menus to the right users, but it is not the thing that protects data. Real protection is enforced in:
- `src/proxy.ts`
- page guards under `src/lib/auth/page-guards.ts`
- session helpers under `src/lib/auth/session.ts`
- route handlers under `src/app/api/**`
## Core Concepts
### User
Authenticated person from Auth.js session.
### Organization
The active tenant/workspace from `session.user.activeOrganizationId`.
### Membership
The user's relation to the active organization. This is loaded server-side and includes:
- `role`
- `permissions`
### Business Role
The UI and feature model mainly uses:
- `HRD`
- `EMPLOYEE`
Business role is derived from session and membership state through `getBusinessRole()` in [src/lib/auth/roles.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/roles.ts).
## Security Boundaries
### 1. Proxy route protection
[src/proxy.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/proxy.ts) blocks unauthenticated access to:
- `/dashboard/**`
- protected API namespaces such as:
- `/api/training-records/**`
- `/api/notifications/**`
- `/api/announcements/**`
- `/api/audit-logs/**`
- other protected modules
This ensures unauthenticated users cannot directly browse protected pages or call protected APIs.
### 2. Server-side session helpers
[src/lib/auth/session.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/session.ts) is the main authorization entry point.
Important helpers:
- `requireSession()`
- `requireOrganizationAccess()`
- `requireHRD()`
- `requireEmployee()`
These helpers:
- validate authentication
- validate active organization context
- verify membership
- derive role-based access
- return organization-scoped access data for downstream queries
### 3. Page-level guards
[src/lib/auth/page-guards.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/page-guards.ts) handles protected dashboard navigation and redirect behavior.
Important guards:
- `requireEmployeeDashboardAccess()`
- `requireHRDDashboardAccess()`
These are used by dashboard pages so direct URL access is also protected, not only sidebar visibility.
### 4. Route handler authorization
Every sensitive route handler must still verify organization and role server-side.
Examples:
- employee-owned training data is scoped by `organizationId` and `userId`
- HRD-only actions use `requireHRD()`
- review and audit routes validate higher privilege before returning data
## Navigation Filtering
Sidebar and kbar filtering are driven by:
- [src/config/nav-config.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts)
- [src/hooks/use-nav.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/hooks/use-nav.ts)
Supported visibility checks in nav items include:
- `requireOrg`
- `systemRole`
- `role`
- `permission`
- `businessRole`
This filtering is based on the current Auth.js session in the client.
Important: hiding a menu item does not grant or remove backend access. It only controls what the user sees in the UI.
## Role-Based Menu Behavior
Current practical behavior:
### Employee
Employee users can see employee-facing areas such as:
- Dashboard
- Training Records
- Announcements
- Notifications
Employee access is still server-scoped to their own allowed data.
### HRD
HRD users can access broader organization operations such as:
- Pending Review
- Employees
- Courses
- Training Policy
- Import Employees
- Reports
- Audit Logs
- Master Review
These pages also require server-side HRD validation, not only visible nav items.
### Super Admin
Super admin users can access system-level organizer management and can be elevated into organization context when required by server-side helpers.
## Direct URL Protection
The system explicitly protects direct navigation to pages even if a user manually enters a URL.
Examples:
- unauthenticated users are redirected at the proxy layer
- authenticated but unauthorized users are redirected by page guards
- API routes return `401` or `403` from server-side helpers when access is invalid
This prevents bypass through copied links or bookmarked URLs.
## API Protection
API protection follows these rules:
1. Authentication is required through Auth.js session
2. Organization membership is verified server-side
3. Role-specific actions require additional server checks
4. Data queries are scoped by organization and, when needed, by the current user
This is the main security boundary for all business data.
## File Download Protection
Protected files must not be served only by direct public URLs.
Current protected examples:
- training certificates:
- `/api/training-records/[id]/certificates/[certificateId]/download`
- announcement attachments:
- `/api/announcements/[id]/attachment`
These endpoints verify:
- authentication
- organization scope
- record-level access
- HRD/employee visibility rules
This prevents unauthorized users from opening files just because they know or guess the storage path.
## Recommended Maintenance Rules
When adding or changing protected features:
1. Add navigation visibility only after server-side access rules are defined
2. Use `requireOrganizationAccess()` for organization-scoped routes
3. Use `requireHRD()` for HRD-only operations
4. Use page guards for dashboard routes
5. Protect file downloads through route handlers when files are sensitive
6. Treat `use-nav.ts` as UX logic, not as a security mechanism
## What To Avoid
Do not reintroduce:
- Clerk-specific authorization assumptions
- client-side-only protection for sensitive features
- public file URLs for protected business documents
- page access that depends only on hidden sidebar items
## Summary
The current model is:
- Auth.js for authentication
- proxy-based route gating for unauthenticated requests
- server-side authorization helpers for real access control
- client-side nav filtering for usability
That combination keeps the UI predictable while ensuring that direct URLs, APIs, and file downloads are protected at the server boundary.

View File

@@ -0,0 +1,164 @@
# Organizers Module Enhancement Report
## Executive Summary
The organizers module was upgraded from a minimal create-and-list page into a CRUD-oriented management flow that supports soft delete, shared validation, audit logging, and runtime filtering for inactive organizers. The implementation keeps the existing Auth.js and Drizzle architecture intact and avoids introducing a new permission pattern that would conflict with the current super-admin entry flow.
## Current Architecture
- Next.js App Router with route handlers under `src/app/api`
- Auth.js session enrichment in `src/auth.ts`
- Drizzle ORM schema in `src/db/schema.ts`
- Feature-owned API/query/mutation contracts in `src/features/organizers`
- Shared UI stack with `Sheet`, `DropdownMenu`, `AlertModal`, and `PageContainer`
## Current Limitation
Before this enhancement, organizers only supported create and basic list rendering. There was no edit action, no soft delete, no inactive filtering in session flows, no feature API layer, and no organizer-specific audit events.
## Root Cause Analysis
- The original organizers page used direct `fetch()` calls and local state instead of the shared feature API/query pattern.
- The database schema did not include soft-delete metadata for organizations.
- Auth session enrichment loaded organizations without filtering inactive records.
- Route handlers existed only for `GET` and `POST`, so update and delete behavior was missing.
## Feature Comparison
Before
- View organizers
- Create organizer
After
- View active and inactive organizers
- Create organizer
- Edit organizer name
- Soft delete organizer
- Validation for required, trim, duplicate, length, and invalid characters
- Audit log for create, update, and soft delete
- Session and switcher filtering for inactive organizers
## Files Modified
- `src/db/schema.ts`
- `drizzle/0017_organizers_soft_delete.sql`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/organizers/api/types.ts`
- `src/features/organizers/api/service.ts`
- `src/features/organizers/api/queries.ts`
- `src/features/organizers/api/mutations.ts`
- `src/features/organizers/schemas/organizer.ts`
- `src/features/organizers/server/organizer-data.ts`
- `src/app/api/organizations/route.ts`
- `src/app/api/organizations/[id]/route.ts`
- `src/app/api/organizations/active/route.ts`
- `src/auth.ts`
- `src/lib/auth/session.ts`
- `src/features/users/server/user-data.ts`
- `src/components/modal/alert-modal.tsx`
- `src/components/org-switcher.tsx`
- `src/features/organizers/components/organizer-form-sheet.tsx`
- `src/features/organizers/components/organizer-card-actions.tsx`
- `src/features/organizers/components/organizer-listing.tsx`
- `src/features/organizers/components/organizers-page.tsx`
- `src/app/dashboard/organizers/page.tsx`
## Database Changes
- Added `organizations.is_active`
- Added `organizations.deleted_at`
- Added `organizations.deleted_by`
- Added foreign key from `deleted_by` to `users.id`
## API Changes
- `GET /api/organizations` supports organizer status filtering
- `POST /api/organizations` uses shared zod validation and duplicate checks
- `PATCH /api/organizations/[id]` updates organizer name
- `DELETE /api/organizations/[id]` performs soft delete only
- `PATCH /api/organizations/active` rejects inactive organizers
## UI Changes
- Replaced the local-state page with a React Query driven listing
- Added shared `Sheet` form for create and edit
- Added shared dropdown row actions
- Added confirmation modal for soft delete
- Added active/inactive status display and management notes
## Permission Changes
- Read access remains aligned to the existing page gate for HRD and super admin
- Create, edit, and soft delete are restricted to `requireSuperAdmin()` in the current architecture
- The implementation intentionally did not force org-scoped permission helpers onto the organizer entry flow because they depend on an active organization
## Validation Changes
- Organizer name is required
- Input is trimmed
- Maximum length is enforced
- Invalid characters are rejected
- Duplicate active organizer names are rejected
- Inactive and missing records are rejected on update/delete
## Soft Delete Design
- Soft delete updates the organizer record instead of physically deleting it
- Disabled organizers are marked with `is_active = false`
- Deletion metadata is stored in `deleted_at` and `deleted_by`
- Related users, memberships, training data, permissions, and audit history remain intact
- Inactive organizers are filtered out from runtime session and switcher flows
## Audit Log
- Added `ORGANIZATIONS` module
- Added `ORGANIZATION_CREATE`
- Added `ORGANIZATION_UPDATE`
- Added `ORGANIZATION_SOFT_DELETE`
## Testing Result
### Create
- Added backend validation and duplicate checks
- Manual runtime verification still required
### Edit
- Added update route and audit logging
- Manual runtime verification still required
### Delete
- Soft delete only
- Inactive organizers are blocked from re-delete
### Permission
- Management actions remain server-protected by super-admin checks
- UI hides management actions for non-managing users
### Regression
- Session loading now excludes inactive organizers
- Organization switching now rejects inactive organizers
- User management lookups now reject inactive target organizations
## Risks
- Existing data with duplicate organizer names may need manual review before relying on new duplicate constraints operationally
- Super-admin users who disable many organizers should still be tested against real seeded data to confirm expected fallback behavior
## Recommendations
- Add focused route-handler tests for organizers create/update/delete flows
- Add browser-level regression coverage for the organizers page and organization switcher
- Consider introducing a dedicated system-scoped permission helper if organizer administration expands beyond super admin
## Future Improvements
- Add search and status filtering controls on the organizers page
- Surface audit history for an organizer directly from the management UI
- Support organization metadata expansion when the schema grows beyond name and plan

View File

@@ -0,0 +1,469 @@
# Permission and Display Audit Report
Audit date: 2026-07-14
Scope: Review-only audit of UI display, navigation, route guards, API authorization, role helpers, permission definitions, and current database permission templates. No runtime source code or database data was changed.
## 1. Executive Summary
Overall assessment: the system is partially aligned with the permission model. Content workflow modules such as announcements and online lessons use `effectivePermissions` and shared workflow helpers consistently for most row actions. However, several HRD operational modules still authorize by `businessRole`, `membership.role`, or broad role helper functions instead of the explicit permission keys that already exist.
Finding count by severity:
| Severity | Count |
| --- | ---: |
| Critical | 0 |
| High | 6 |
| Medium | 5 |
| Low | 3 |
Highest risk modules:
| Module | Risk |
| --- | --- |
| Employee import and master review | Explicit permissions exist but route/page/API guards use HRD role only. |
| Training records review and reports | Organization-wide access and review are derived from membership role instead of granular permissions. |
| Training matrix and courses | Some routes use role guards while navigation suggests permission-aware access. |
| Navigation | Sidebar uses role/businessRole for many items, while pages and APIs sometimes use different checks. |
Data exposure observed:
- Reports can expose organization-wide data when `membership.role` is `admin`, even if the active permission template only grants `reports:self_read`.
- Employee directory and master-data review areas are visible to HRD-role users even when the template does not include `employee_import:run` or `employee_master_review:approve`.
Backend authorization summary:
- Announcements, online lessons, training policies, training record import, and permission management have meaningful permission checks.
- Courses, employee directory, employee import, master review, training matrix, training record review, reports, audit logs, and users still contain role-based or membership-role authorization seams.
## 2. Roles and Permissions Discovered
Role sources:
| Source | Evidence | Behavior |
| --- | --- | --- |
| `systemRole` | `src/lib/auth/roles.ts` | `super_admin` is treated as IT-like and HRD-capable in some helpers. |
| `membership.role` | `src/db/schema.ts`, `src/lib/auth/roles.ts` | `owner`/`admin` imply HRD; `member` implies EMPLOYEE. |
| `businessRole` | `src/lib/auth/roles.ts` | Derived from system and membership role, not from permission template. |
| `effectivePermissions` | `src/auth.ts`, `src/lib/auth/authorization.server.ts` | Resolved from active permission template, overrides, or fallback membership/default permissions. |
Permission definition source:
- `src/lib/auth/permissions.ts` defines all permission keys and defaults.
- `src/features/permission-management/server/permission-management-catalog.ts` defines catalog metadata.
- `permission_templates`, `permission_template_permissions`, `user_permission_template_assignments`, and `user_permission_overrides` provide database-configured permissions.
Important discovered permission groups:
| Group | Examples |
| --- | --- |
| Employee directory/import | `employee_directory:read`, `employee_directory:write`, `employee_import:run`, `employee_master_review:approve` |
| Courses and policy | `courses:read`, `courses:write`, `training_policy:read`, `training_policy:write` |
| Training matrix | `training_matrix:read`, `training_matrix:write` |
| Training records | `training_record:self_read`, `training_record:read_all`, `training_record:write_all`, `training_record:review`, `training_record:approve`, `training_record:reject` |
| Announcements | `announcement:create`, `announcement:edit_draft`, `announcement:approve`, `announcement:revision`, `announcement:publish` |
| Online lessons | `online_lesson:create`, `online_lesson:edit_draft`, `online_lesson:approve`, `online_lesson:revision`, `online_lesson:publish` |
| Reports | `reports:self_read`, `reports:organization_read`, `reports:export`, `reports:export_excel`, `reports:export_pdf` |
| Permission management | `permission_template:*`, `user_permission:*` |
Database template observations:
| Template | Count | Notable permissions |
| --- | ---: | --- |
| `employee` | 8 | Self training record, public announcements, public online lessons, self reports, notifications. |
| `hrd-staff` id 2 | 42 | Includes `training_record:approve`, `training_record:review`, `reports:organization_read`, content publishing but excludes `employee_import:run` and `employee_master_review:approve`. |
| `hrd-staff` id 5/id 8 | 30 | Includes HRD write/read basics but excludes review/approve/report-org and employee import/master-review permissions. |
| `manager` | 56 | Includes training review, reports export, online lesson approve/revision/reject, announcement approve/revision/reject. |
| `IT` | 78 | Broad system, permission management, audit, import, master review, and all content workflow permissions. |
## 3. Permission Matrix
| Module | UI/Page Entry | Primary Page Guard | API Guard | Permission Alignment |
| --- | --- | --- | --- | --- |
| Overview | `requireOrg` nav | Employee dashboard access | Server report/overview scope | Medium: mixed role/permission usage. |
| Users | IT nav | User management access | user management helper | Medium: helper is IT/business-role based, not `users:*`. |
| Employee directory | HRD/IT nav | HRD dashboard access | `requireHRD()` | High mismatch: ignores `employee_directory:*`. |
| Employee import | hidden/direct route | HRD dashboard access | `requireHRD()` | High mismatch: ignores `employee_import:run`. |
| Master review | hidden/direct route | HRD dashboard access | `requireHRD()` | High mismatch: ignores `employee_master_review:approve`. |
| Courses | HRD/IT nav | HRD dashboard access | `requireHRD()` | High mismatch: ignores `courses:read/write`. |
| Training policy | HRD/IT nav | `training_policy:read` | `training_policy:read/write` | Good alignment. |
| Training matrix | HRD/IT nav | `training_matrix:read` | `membership.role` via `canManageTrainingMatrix()` | High mismatch between page guard and API. |
| Training records list | all org users | Employee dashboard access | membership role and data helper | Medium: permissions exist but access logic is role based. |
| Training records import | conditional button | `write_all + approve` | `write_all + approve` | Good alignment. |
| Pending review | HRD nav | HRD dashboard access | training record list query | Medium mismatch: page/nav role based, review permission not required. |
| Reports | HRD nav, direct employee page | Employee dashboard access | report scope by role | High mismatch: ignores `reports:organization_read/export`. |
| Announcements | HRD/IT nav | Employee dashboard access plus action permissions | content workflow permissions | Mostly aligned. |
| Online lessons | all org users | Employee dashboard access plus action permissions | content workflow permissions | Mostly aligned. |
| Audit logs | super admin nav | IT dashboard access | `requireIT()` | Medium mismatch: permission keys exist but not used. |
| Permission management | super admin nav | super admin dashboard access | assumed super admin route guards | Low: intentionally super admin-only, not permission-template based. |
## 4. Route and Navigation Audit
| ID | Route | Menu Permission | Route Guard | Direct URL Result | Status |
| --- | --- | --- | --- | --- | --- |
| NAV-001 | `/dashboard/pending-review` | `businessRole: HRD` | `requireHRDDashboardAccess()` | HRD-role users can enter without checking `training_record:review` | Mismatch |
| NAV-002 | `/dashboard/reports` | `businessRole: HRD` | `requireEmployeeDashboardAccess()` | Employees can enter directly; dataset scopes by membership role | Mismatch |
| NAV-003 | `/dashboard/employee-directory` | `businessRoles: HRD, IT` | `requireHRDDashboardAccess()` | HRD-role users can enter without `employee_directory:read` | Mismatch |
| NAV-004 | `/dashboard/courses` | `businessRoles: HRD, IT` | `requireHRDDashboardAccess()` | HRD-role users can enter without `courses:read` | Mismatch |
| NAV-005 | `/dashboard/training-policy` | `businessRoles: HRD, IT` | `requirePermissionDashboardAccess('training_policy:read')` | Users with nav role but missing permission would see menu but be redirected | UI/route mismatch possible |
| NAV-006 | `/dashboard/training-matrix` | `businessRoles: HRD, IT` | `requirePermissionDashboardAccess('training_matrix:read')` | Menu is role-based; route is permission-based | UI/route mismatch possible |
| NAV-007 | `/dashboard/import-employees` | No sidebar item found | `requireHRDDashboardAccess()` | HRD-role users can enter directly without `employee_import:run` | Hidden but accessible |
| NAV-008 | `/dashboard/master-review` | No sidebar item found | `requireHRDDashboardAccess()` | HRD-role users can enter directly without `employee_master_review:approve` | Hidden but accessible |
| NAV-009 | `/dashboard/audit-logs` | `systemRole: super_admin` | `requireITDashboardAccess()` | IT-like users may satisfy page guard depending business role | Mismatch |
| NAV-010 | `/dashboard/online-lessons` | `requireOrg` | Employee dashboard access plus action permissions | Direct URL aligns for read; actions are permission checked | Acceptable |
| NAV-011 | `/dashboard/announcements` | `businessRoles: HRD, IT` | Employee dashboard access plus action permissions | Read access can be broad; create/action buttons use permissions | Mostly aligned |
## 5. UI Display Audit
| ID | Module | Page / Component | Element | Expected | Actual | Severity | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- |
| UI-001 | Navigation | Sidebar | HRD module links | Show by explicit permission where permission exists | Many HRD links show by `businessRoles` | Medium | `src/config/nav-config.ts` |
| UI-002 | Training records | Listing page | Review queue button | Show only with review permission | Uses `isHRD()` | Medium | `src/app/dashboard/training-records/page.tsx` |
| UI-003 | Training records | Pending review page | Page visibility | Require `training_record:review` or approve/reject permissions | Requires HRD role only | Medium | `src/app/dashboard/pending-review/page.tsx` |
| UI-004 | Employee import | Direct page | Page visibility | Require `employee_import:run` | Requires HRD role only | High | `src/app/dashboard/import-employees/page.tsx` |
| UI-005 | Master review | Direct page | Page visibility | Require `employee_master_review:approve` | Requires HRD role only | High | `src/app/dashboard/master-review/page.tsx` |
| UI-006 | Courses | Listing and create link | Create/write access | Require `courses:write` for write UI | HRD page role gate; action visibility not permission-scoped at page level | Medium | `src/app/dashboard/courses/page.tsx` |
| UI-007 | Reports | Reports page | Organization filters and organization-wide data | Require `reports:organization_read` | Uses membership role for org-wide access | High | `src/features/reports/server/report-data.ts` |
| UI-008 | Training policy | Page and actions | Read/write | Uses permission guard and write permission in component | Aligned | Low | `src/app/dashboard/training-policy/page.tsx`, `src/features/training-policy/components/training-policy-page.tsx` |
| UI-009 | Online lessons | Action menu | edit/submit/approve/return/reject/publish | Use content workflow permissions and status | Aligned | Low | `src/features/online-lessons/components/online-lesson-action-menu.tsx` |
| UI-010 | Announcements | Action menu | edit/submit/approve/return/reject/publish | Use content workflow permissions and status | Aligned | Low | `src/features/announcements/components/announcement-action-menu.tsx` |
## 6. Backend Authorization Audit
| ID | Endpoint / Action | Authentication | Permission | Ownership | Status Guard | Result |
| --- | --- | --- | --- | --- | --- | --- |
| API-001 | `POST /api/import-employees` | `requireHRD()` | None for `employee_import:run` | Organization scoped | File validation only | High mismatch |
| API-002 | `GET /api/import-employees/template` | `requireHRD()` | None for `employee_import:run` | N/A | N/A | High mismatch |
| API-003 | `GET/PATCH /api/master-review` | `requireHRD()` | None for `employee_master_review:approve` | Organization scoped | Entity action check | High mismatch |
| API-004 | `GET/POST/PATCH/DELETE /api/courses` | `requireHRD()` or `isHRD()` | None for `courses:read/write` | Organization scoped | N/A | High mismatch |
| API-005 | `/api/employee-directory` and targets | `requireHRD()` | None for `employee_directory:read/write` | Organization scoped | N/A | High mismatch |
| API-006 | `/api/training-matrix*` | `requireOrganizationAccess()` | Uses `membership.role`, not `training_matrix:*` | Organization scoped | N/A | High mismatch |
| API-007 | `/api/training-records/[id]/review` | `requireOrganizationAccess()` | Uses `canReviewTrainingRecords(membership.role)` | Organization scoped | Review status guard in data layer | High mismatch |
| API-008 | `/api/training-records/import` | `requireAllPermissions(['training_record:write_all','training_record:approve'])` | Explicit | Organization scoped | Upload validation | Aligned |
| API-009 | `/api/training-policies*` | `requirePermission()` | Explicit read/write | Organization scoped | Active/clone/deactivate guards | Aligned |
| API-010 | `/api/announcements*` | Organization access plus workflow helpers | Explicit content permissions | Owner and status checked | Workflow status checked | Aligned |
| API-011 | `/api/online-lessons*` | Organization access plus workflow helpers | Explicit content permissions | Owner and status checked | Workflow status checked | Aligned |
| API-012 | `/api/reports/export` | Not fully reviewed in detail | Permission keys exist | Requires confirmation | Requires confirmation | Needs follow-up |
| API-013 | `/api/audit-logs*` | `requireIT()` | Does not use `audit_logs:*` | Organization scoped | N/A | Medium mismatch |
| API-014 | `/api/users*` | User management helper | Helper is IT/businessRole based | Organization scoped | N/A | Medium mismatch |
## 7. Findings
### PERM-001: HRD role grants employee import without `employee_import:run`
- Severity: High
- Module: Employee import
- Affected roles: HRD users without `employee_import:run`, including some `hrd-staff` templates.
- Expected behavior: page and API should require `employee_import:run`.
- Actual behavior: page uses `requireHRDDashboardAccess()` and API uses `requireHRD()`.
- Security or business impact: HRD-role users can import employee/training data by direct URL/API even if the configured template does not grant import.
- Root cause: role guard is used instead of permission guard.
- Evidence: `employee_import:run` exists in `src/lib/auth/permissions.ts`; guards are in `src/app/dashboard/import-employees/page.tsx` and `src/app/api/import-employees/route.ts`.
- File path: `src/app/dashboard/import-employees/page.tsx`, `src/app/api/import-employees/route.ts`, `src/app/api/import-employees/template/route.ts`
- Line or code reference: page line 6, route line 454, template route line 12.
- Reproduction steps: sign in as HRD-role user with template lacking `employee_import:run`, open `/dashboard/import-employees`, submit Excel to `/api/import-employees`.
- Recommended remediation: replace page/API guards with `requirePermissionDashboardAccess('employee_import:run')` and `requirePermission('employee_import:run')`.
- Related findings: PERM-002, PERM-003.
### PERM-002: Master review does not require `employee_master_review:approve`
- Severity: High
- Module: Master review
- Affected roles: HRD users without `employee_master_review:approve`.
- Expected behavior: viewing and approving/rejecting imported master data should require `employee_master_review:approve`.
- Actual behavior: page and API use HRD role only.
- Security or business impact: users can approve/reject company, department, and position changes beyond configured template permissions.
- Root cause: permission key exists but is not enforced.
- Evidence: permission key in `src/lib/auth/permissions.ts`; route guards in `src/app/dashboard/master-review/page.tsx` and `src/app/api/master-review/route.ts`.
- File path: `src/app/dashboard/master-review/page.tsx`, `src/app/api/master-review/route.ts`
- Line or code reference: page line 9, API lines 42 and 137.
- Reproduction steps: HRD user without `employee_master_review:approve` opens `/dashboard/master-review`, calls PATCH `/api/master-review`.
- Recommended remediation: require `employee_master_review:approve` on page and API.
- Related findings: PERM-001.
### PERM-003: Employee directory read/write ignores `employee_directory:*`
- Severity: High
- Module: Employee directory
- Affected roles: HRD users whose effective template excludes directory read/write.
- Expected behavior: list/detail requires `employee_directory:read`; create/edit/targets require `employee_directory:write`.
- Actual behavior: dashboard pages and API use HRD role only.
- Security or business impact: broad employee profile and target management access can exceed template configuration.
- Root cause: legacy HRD guard remains after permission keys were introduced.
- Evidence: `employee_directory:read/write` exist in `src/lib/auth/permissions.ts`; route uses `requireHRD()` in `src/app/api/employee-directory/route.ts` and target route.
- File path: `src/app/dashboard/employee-directory/page.tsx`, `src/app/api/employee-directory/route.ts`, `src/app/api/employee-directory/[id]/target/route.ts`
- Line or code reference: dashboard line 17, API line 7, target API lines 27 and 62.
- Reproduction steps: HRD user without employee directory permissions opens `/dashboard/employee-directory` or calls `/api/employee-directory`.
- Recommended remediation: map read/write operations to `employee_directory:read` and `employee_directory:write`.
- Related findings: PERM-001.
### PERM-004: Courses use HRD role instead of `courses:read/write`
- Severity: High
- Module: Courses
- Affected roles: HRD users without course permissions; users with `courses:*` but not HRD.
- Expected behavior: list/detail require `courses:read`; create/update/delete require `courses:write`.
- Actual behavior: dashboard and API use HRD role checks.
- Security or business impact: permission templates cannot precisely control course access.
- Root cause: role-based feature guard predates explicit permission keys.
- Evidence: `courses:read/write` in `src/lib/auth/permissions.ts`; page uses `requireHRDDashboardAccess()`; API uses `requireHRD()` and `isHRD()`.
- File path: `src/app/dashboard/courses/page.tsx`, `src/app/api/courses/route.ts`, `src/app/api/courses/[id]/route.ts`
- Line or code reference: page line 20, API route lines 22 and 64.
- Reproduction steps: assign user template with/without `courses:*` and compare UI/API with HRD membership role.
- Recommended remediation: use `requirePermissionDashboardAccess('courses:read')` for read pages and `requirePermission('courses:write')` for mutations.
- Related findings: PERM-006.
### PERM-005: Training matrix page guard uses permissions but API uses membership role
- Severity: High
- Module: Training matrix
- Affected roles: users with `training_matrix:read/write` but non-admin membership, and admin users without template permission.
- Expected behavior: API should enforce `training_matrix:read` or `training_matrix:write`.
- Actual behavior: API calls `canManageTrainingMatrix(membership.role)`.
- Security or business impact: permission template changes may not affect API access; UI and backend can disagree.
- Root cause: split guard model between page and API.
- Evidence: page uses `requirePermissionDashboardAccess('training_matrix:read')`; API uses `canManageTrainingMatrix(membership.role)`.
- File path: `src/app/dashboard/training-matrix/page.tsx`, `src/features/training-matrix/server/training-matrix-data.ts`, `src/app/api/training-matrix/route.ts`
- Line or code reference: page line 20, helper line 62, API lines 17 and 54.
- Reproduction steps: grant `training_matrix:*` to a member-role user or deny it to an admin-role user, then call API directly.
- Recommended remediation: replace role helper at API boundary with `requirePermission()` checks.
- Related findings: PERM-004.
### PERM-006: Training record review uses membership role instead of review permissions
- Severity: High
- Module: Training records
- Affected roles: admin-role users without `training_record:review/approve/reject`, and non-admin users with explicit review permissions.
- Expected behavior: review queue and review API should require explicit review/approve/reject permissions.
- Actual behavior: review eligibility uses `isHRD()` or `canReviewTrainingRecords(membership.role)`.
- Security or business impact: review authority is tied to membership role and can exceed template configuration.
- Root cause: old membership-role model remains in canonical training-record data helpers.
- Evidence: `canReviewTrainingRecords(role)` delegates to owner/admin role; pending-review page uses HRD guard.
- File path: `src/features/training-records/server/training-record-data.ts`, `src/app/dashboard/pending-review/page.tsx`, `src/app/api/training-records/[id]/review/route.ts`
- Line or code reference: helper line 132, pending page line 20, review API line 34.
- Reproduction steps: HRD/admin user lacking `training_record:review` opens pending review and calls review API.
- Recommended remediation: introduce permission-aware review checks using `training_record:review`, `training_record:approve`, and `training_record:reject`.
- Related findings: PERM-007.
### PERM-007: Reports organization scope ignores `reports:organization_read`
- Severity: High
- Module: Reports
- Affected roles: admin-role users with only `reports:self_read`, and non-admin users with `reports:organization_read`.
- Expected behavior: organization-wide reports require `reports:organization_read`; export requires export permissions.
- Actual behavior: organization scope is based on membership role through `canViewAllReports(role)`.
- Security or business impact: admin-role users can see organization-wide reports even when the template grants only self reports.
- Root cause: reports scope uses membership role instead of permission keys.
- Evidence: `reports:organization_read` exists in permissions; `canViewAllReports(role)` uses owner/admin.
- File path: `src/features/reports/server/report-data.ts`, `src/app/dashboard/reports/page.tsx`
- Line or code reference: report helper line 61, dashboard page line 20.
- Reproduction steps: HRD staff template id 5/id 8 has only `reports:self_read` but membership role admin can still be scoped as all by role.
- Recommended remediation: calculate report scope from `effectivePermissions`.
- Related findings: PERM-006.
### PERM-008: Sidebar uses business roles where explicit permissions exist
- Severity: Medium
- Module: Navigation
- Affected roles: users whose templates diverge from membership roles.
- Expected behavior: sidebar should hide/show entries based on the same permission used by page/API where possible.
- Actual behavior: many items use `businessRole` or `businessRoles`.
- Security or business impact: users may see menu items they cannot use, or not see items they can use by direct URL.
- Root cause: nav configuration predates fine-grained permission templates.
- Evidence: `src/config/nav-config.ts` uses `businessRole: "HRD"` and `businessRoles: ["HRD","IT"]`.
- File path: `src/config/nav-config.ts`, `src/hooks/use-nav.ts`
- Line or code reference: nav lines 23, 64, 72, 80, 88, 96, 99, 159.
- Reproduction steps: assign a template with read permission but non-HRD membership role, or remove a permission from HRD template.
- Recommended remediation: prefer `access.permission` for permission-backed modules; keep role access only for role-only concepts.
- Related findings: PERM-004, PERM-005.
### PERM-009: `use-nav` reads `session.user.permissions` instead of explicitly using `effectivePermissions`
- Severity: Medium
- Module: Navigation
- Affected roles: users with allow/deny overrides if `permissions` ever diverges from `effectivePermissions`.
- Expected behavior: permission-based nav should use effective permissions.
- Actual behavior: nav access context uses `session.user.permissions`.
- Security or business impact: low current risk because Auth.js currently assigns both fields to effective permissions, but maintainability risk if they diverge later.
- Root cause: naming ambiguity between permissions and effectivePermissions.
- Evidence: `src/auth.ts` currently assigns both; `src/hooks/use-nav.ts` uses `session.user.permissions`.
- File path: `src/auth.ts`, `src/hooks/use-nav.ts`
- Line or code reference: auth lines 265-298, hook line 106.
- Reproduction steps: future change separates raw permissions from effective permissions.
- Recommended remediation: use `session.user.effectivePermissions` in navigation.
- Related findings: PERM-008.
### PERM-010: Audit logs use IT role instead of `audit_logs:*`
- Severity: Medium
- Module: Audit logs
- Affected roles: users with audit permissions but not IT role; IT users without explicit audit permissions.
- Expected behavior: read/export require `audit_logs:read` and `audit_logs:export`.
- Actual behavior: page and APIs use IT role helpers.
- Security or business impact: permission templates cannot independently control audit access.
- Root cause: audit module is role-gated.
- Evidence: `audit_logs:*` keys exist; audit page/API use IT guard.
- File path: `src/app/dashboard/audit-logs/page.tsx`, `src/app/api/audit-logs/route.ts`, `src/app/api/audit-logs/export/route.ts`
- Line or code reference: dashboard line 16, API line 8, export API line 16.
- Reproduction steps: grant `audit_logs:read` to non-IT or remove audit permissions from IT template.
- Recommended remediation: use permission guards while preserving super admin fallback if required.
- Related findings: PERM-008.
### PERM-011: User management helper is role-based despite `users:*` permissions
- Severity: Medium
- Module: Users
- Affected roles: users with `users:*` but not IT role, and IT-role users without `users:*`.
- Expected behavior: users list and mutations should enforce `users:read/create/update/delete/manage`.
- Actual behavior: `requireUserManagementAccess()` delegates to `canManageUsers()`, which checks IT role.
- Security or business impact: permission templates cannot precisely delegate user-management tasks.
- Root cause: business-role helper is used as the source of truth.
- Evidence: `users:*` keys exist; `canManageUsers()` returns `isIT()`.
- File path: `src/lib/auth/session.ts`, `src/lib/auth/roles.ts`, `src/app/dashboard/users/page.tsx`
- Line or code reference: session line 199, roles line 94, users page line 18.
- Reproduction steps: assign a non-IT user `users:read`; user still cannot access. Remove `users:*` from IT template; IT role may still pass helper.
- Recommended remediation: introduce permission-specific user management guards.
- Related findings: PERM-010.
### PERM-012: Permission templates with same code differ across organizations
- Severity: Low
- Module: Permission management / operations
- Affected roles: users assigned `hrd-staff` in different organizations.
- Expected behavior: same template code may vary by organization, but reports should make this explicit.
- Actual behavior: DB has `hrd-staff` with 42 permissions in one org and 30 in others.
- Security or business impact: operators may assume "HRD Staff" means the same capability everywhere.
- Root cause: organization-scoped templates share code/name but differ in permission sets.
- Evidence: database query returned `hrd-staff` id 2 with 42 permissions, id 5/id 8 with 30 permissions.
- File path: database tables `permission_templates`, `permission_template_permissions`.
- Line or code reference: schema tables in `src/db/schema.ts`.
- Reproduction steps: compare permission templates by `code = 'hrd-staff'` across organizations.
- Recommended remediation: document organization-scoped template differences and add UI comparison/audit affordance.
- Related findings: PERM-008.
### PERM-013: Direct hidden routes exist for HRD-only workflows
- Severity: Medium
- Module: Navigation / routes
- Affected roles: HRD users without explicit hidden-route permissions.
- Expected behavior: hidden routes should still enforce exact permissions.
- Actual behavior: hidden import/master-review routes are not sidebar-visible but are accessible by direct URL to HRD role.
- Security or business impact: hiding the menu does not prevent access.
- Root cause: route guards are less strict than intended permission keys.
- Evidence: `/dashboard/import-employees` and `/dashboard/master-review` are not in sidebar but have HRD-only guards.
- File path: `src/config/nav-config.ts`, `src/app/dashboard/import-employees/page.tsx`, `src/app/dashboard/master-review/page.tsx`
- Line or code reference: nav does not include these routes; page lines 6 and 9.
- Reproduction steps: manually type direct route URL as HRD staff.
- Recommended remediation: direct routes must require explicit permission, regardless of sidebar visibility.
- Related findings: PERM-001, PERM-002.
### PERM-014: Content workflow modules are comparatively well aligned
- Severity: Low
- Module: Announcements and online lessons
- Affected roles: content creators, reviewers, publishers.
- Expected behavior: status transitions and row actions use explicit permissions.
- Actual behavior: shared workflow helpers map actions to module-specific permissions and are used in UI/API.
- Security or business impact: low; this is a positive control area and should be used as a reference.
- Root cause: centralized content workflow helper.
- Evidence: `canEditContent`, `canDeleteContent`, and `canTransitionContent` are used by action menus and APIs.
- File path: `src/features/content-approval/lib/workflow.ts`, online lesson/announcement components and APIs.
- Line or code reference: workflow lines 109-168.
- Reproduction steps: compare action menu and API guards for status transitions.
- Recommended remediation: reuse this pattern for other modules.
- Related findings: PERM-004 to PERM-011.
## 8. Role-based Test Scenarios
| Test ID | Role / Template | Page | Action | Expected Result |
| --- | --- | --- | --- | --- |
| T-001 | Employee template | `/dashboard/online-lessons` | View public lesson | Allowed if published/public. |
| T-002 | Employee template | `/dashboard/online-lessons/manage/new` | Create lesson | Denied without `online_lesson:create`. |
| T-003 | HRD Staff id 5/id 8 | `/dashboard/import-employees` | Direct URL | Should be denied because template lacks `employee_import:run`. Current likely allowed by HRD role. |
| T-004 | HRD Staff id 5/id 8 | `/dashboard/master-review` | Approve/reject master data | Should be denied because template lacks `employee_master_review:approve`. Current likely allowed by HRD role. |
| T-005 | HRD Staff id 5/id 8 | `/dashboard/reports` | Organization-wide report | Should be denied because template lacks `reports:organization_read`. Current may be allowed by admin membership role. |
| T-006 | HRD Staff id 5/id 8 | `/dashboard/pending-review` | Open review queue | Should require `training_record:review`; current uses HRD role. |
| T-007 | HRD Staff id 2 | `/dashboard/training-records/import` | Import records | Allowed because template includes `training_record:write_all` and `training_record:approve`. |
| T-008 | HRD Manager | Online lesson pending item | Return for revision | Allowed with `online_lesson:revision`. |
| T-009 | HRD Staff | Returned own online lesson | Edit and resubmit | Allowed with `online_lesson:edit_draft` and `online_lesson:submit`. |
| T-010 | Non-owner HRD Staff | Other user's returned online lesson | Edit | Denied unless a future manage permission is introduced. |
| T-011 | Member with `training_matrix:read` override | `/dashboard/training-matrix` | Open page and call API | Page should allow; current API may deny by membership role. |
| T-012 | IT template | `/dashboard/audit-logs` | Read/export audit logs | Allowed currently by IT role; should also be verified against `audit_logs:*`. |
| T-013 | Super admin | Permission management | Manage templates/users | Allowed by super admin. |
## 9. Prioritized Remediation Plan
### Priority 0 - Critical Security
No confirmed Critical findings in this audit. The most serious confirmed issues are High permission mismatches where broad HRD/admin roles override finer-grained permission templates.
### Priority 1 - Permission Mismatch
1. Replace employee import page/API guards with `employee_import:run`.
2. Replace master review page/API guards with `employee_master_review:approve`.
3. Replace courses page/API guards with `courses:read` and `courses:write`.
4. Replace employee directory read/write guards with `employee_directory:read` and `employee_directory:write`.
5. Replace training matrix API role helper with `training_matrix:read/write`.
6. Replace training record review role helper with `training_record:review/approve/reject`.
7. Replace report organization-wide scope with `reports:organization_read` and export checks with export permissions.
### Priority 2 - Data Exposure
1. Review reports data scope first because it can expose organization-wide data through membership role.
2. Review employee-directory detail and target routes for employee profile/target exposure.
3. Review hidden HRD routes and enforce exact permissions.
### Priority 3 - Maintainability
1. Update sidebar config to use `access.permission` where explicit permissions exist.
2. Use `effectivePermissions` consistently in navigation.
3. Extract module-level permission helpers for courses, employee directory, reports, training matrix, and training records.
4. Use content workflow helper pattern as a reference for other workflow modules.
## 10. Files Reviewed
| File | Reason |
| --- | --- |
| `plans/system-ui-permission-review-plan.md` | Audit requirements and report structure. |
| `.agents/skills/kiranism-shadcn-dashboard/SKILL.md` | Project dashboard/RBAC guidance. |
| `docs/AI_DEVELOPMENT_GUIDE.md` | Project implementation conventions. |
| `docs/PROJECT_ARCHITECTURE.md` | Architecture and auth/RBAC context. |
| `src/lib/auth/permissions.ts` | Permission definitions and defaults. |
| `src/lib/auth/roles.ts` | Business role and membership role mapping. |
| `src/lib/auth/session.ts` | Server auth guard behavior. |
| `src/lib/auth/authorization.server.ts` | Effective permission resolution. |
| `src/auth.ts` | Auth.js session enrichment and effective permission assignment. |
| `src/config/nav-config.ts` | Sidebar visibility. |
| `src/hooks/use-nav.ts` | Client navigation filtering. |
| `src/features/content-approval/lib/workflow.ts` | Content workflow permission helper. |
| `src/features/online-lessons/components/online-lesson-action-menu.tsx` | Online lesson UI action checks. |
| `src/features/announcements/components/announcement-action-menu.tsx` | Announcement UI action checks. |
| `src/features/training-records/server/training-record-data.ts` | Training record access/review logic. |
| `src/features/training-matrix/server/training-matrix-data.ts` | Training matrix role helper. |
| `src/features/reports/server/report-data.ts` | Report scope logic. |
| `src/app/dashboard/**/page.tsx` | Route/page guards. |
| `src/app/api/**/route.ts` | API authorization patterns. |
| Database permission templates | Current configured role/template reality. |
## 11. Files Not Reviewed or Limitations
- Manual browser verification was not performed in this pass.
- API calls were not executed as each role; findings are based on static code inspection and database configuration.
- Report export route was not fully expanded; it should be reviewed in a follow-up because export permission keys exist.
- Legacy/template modules such as products, forms, kanban, chat, and billing were not deeply audited because they are outside the current training-system permission core.
- Some Thai text appears mojibake in terminal output because of encoding, but file paths and authorization logic were readable.
## 12. Final Assessment
The system is not yet fully permission-first. It is safe in many content workflow areas because online lessons and announcements consistently use `effectivePermissions`, ownership, and status transition helpers. The main readiness gap is older HRD operational modules that still trust `businessRole` or `membership.role`.
Before production hardening, fix the High findings around employee import, master review, employee directory, courses, training matrix API, training record review, and reports scope. The most urgent business risk is that users with HRD/admin membership can access or act on data beyond what their assigned permission template appears to grant.
Confirmed deliverables:
- Found 14 findings: 0 Critical, 6 High, 5 Medium, 3 Low.
- Highest risk modules: employee import, master review, reports, training records review, training matrix API, employee directory.
- Data exposure risk exists for reports and employee directory/master-data review.
- No API route was found with no authentication at all in the reviewed core modules, but several APIs enforce broad role guards instead of explicit permissions.
- This report was created at `permission-display-audit.md`.
- No source code behavior or database data was changed during the audit.

View File

@@ -0,0 +1,99 @@
# Permission Matrix UI Improvement Report
วันที่อัปเดต: 2026-07-07
## 1. Summary
ปรับปรุงหน้า `Permission Matrix` และ Permission Catalog ให้ผู้ใช้งานเข้าใจ permission แต่ละรายการได้ง่ายขึ้น โดยเพิ่ม metadata ระดับคำอธิบายภาษาไทยให้ทุก permission และปรับ UI ให้แสดงข้อมูลตามลำดับ:
- Permission Name
- Description
- Permission Code
- Action Badge
รอบนี้เป็นการปรับปรุงเฉพาะ UI/UX และ catalog metadata เท่านั้น โดยไม่เปลี่ยน business logic, permission code, authorization, API contract หลัก หรือ database schema
## 2. Files Modified
- `src/features/permission-management/api/types.ts`
- `src/features/permission-management/server/permission-management-catalog.ts`
- `src/features/permission-management/components/permission-matrix.tsx`
- `src/features/permission-management/components/permission-template-form-sheet.tsx`
## 3. UI Changes
สิ่งที่เปลี่ยนในหน้า `Permission Matrix`:
- เปลี่ยนจากแสดงแค่ `action` + `permission code`
- เพิ่ม `permission.description` ภาษาไทยในแต่ละ card
- แสดง `permission.code` ด้วย `font-mono`
- ปรับ typography:
- name ใช้ `text-base font-semibold`
- description ใช้ `text-sm text-muted-foreground`
- code ใช้ `font-mono text-xs`
- คง `Action` badge เดิมไว้
ผลลัพธ์คือผู้ใช้สามารถอ่านแล้วเข้าใจได้ทันทีว่า permission นั้น “ใช้ทำอะไร” โดยไม่ต้องเดาจาก code
นอกจากนี้ใน `Permission Template Form` ก็เปลี่ยนให้แสดง description จาก catalog เดียวกันด้วย เพื่อไม่ hardcode ข้อความและรองรับการใช้งานต่อในอนาคต
## 4. Permission Catalog Changes
ปรับ catalog เดิมใน `src/features/permission-management/server/permission-management-catalog.ts` โดยเพิ่ม metadata ต่อ permission ดังนี้:
- `module`
- `action`
- `code`
- `name`
- `description`
- `descriptionEn`
และยังคง field compatibility เดิมไว้ด้วย:
- `value`
- `label`
ดังนั้น component เดิมที่อิง `permission.value` ยังใช้ต่อได้ และ component ใหม่สามารถอ่าน `permission.description` ได้ทันที
## 5. Permission Coverage
ผลการตรวจสอบ catalog:
- จำนวน permission ทั้งหมดในระบบ: `78`
- จำนวน permission ที่มี metadata ครบ: `78`
- จำนวน permission ที่ขาดคำอธิบาย: `0`
- จำนวน permission ที่ขาด field สำคัญ (`module`, `action`, `code`, `name`, `description`): `0`
ยืนยันว่า permission ทุกตัวมีข้อมูลครบ:
- `module`
- `action`
- `code`
- `name`
- `description`
## 6. Compatibility
ยืนยันว่าในการเปลี่ยนรอบนี้:
- Permission Code ไม่เปลี่ยน
- Business Logic ไม่เปลี่ยน
- Authorization ไม่เปลี่ยน
- API ไม่เปลี่ยนในเชิงพฤติกรรมการอนุญาต
- Database ไม่เปลี่ยน
รอบนี้เป็น UX improvement ของ catalog และการ render UI เท่านั้น
## 7. Verification
ตรวจสอบแล้ว:
- `npx oxlint src/features/permission-management src/app/api/permissions src/features/permission-management/server/permission-management-catalog.ts` ผ่าน
- `npx tsc --noEmit` ผ่าน
- coverage check ของ permission catalog:
- total permissions = `78`
- coverage = `78`
- missing = `0`
- incomplete = `0`
- หน้า `Permission Matrix` แสดง description จาก `permission.description`
- Permission Code ยังคงเดิมทุกตัว

View File

@@ -0,0 +1,207 @@
# Permission Template Authorization Implementation
วันที่อัปเดต: 2026-07-07
## Scope ของเฟสแรก
เฟสแรกของงาน `permission-authorization` เน้นวาง foundation ให้ระบบย้ายจาก role-based authorization ไปสู่ permission-first model แบบค่อยเป็นค่อยไป โดยยังไม่บังคับเปลี่ยนทุก feature พร้อมกันในครั้งเดียว
สิ่งที่ทำในเฟสนี้:
- เพิ่ม permission catalog กลาง
- เพิ่ม helper สำหรับเช็ก permission แบบ type-safe
- ผูก default permission ของ membership เข้ากับ catalog กลาง
- เพิ่ม session helper สำหรับ `requirePermission()` / `requireAnyPermission()` / `requireAllPermissions()`
- เชื่อม `src/auth.ts` ให้ resolve effective permissions จาก template assignment และ override
- ขยาย session payload ให้มี `effectivePermissions` และ `permissionTemplateIds`
- เพิ่ม schema และ migration สำหรับ permission template, assignment, override, และ audit log
## โครงสร้าง Permission-first Model
โครงสร้างเป้าหมายของระบบ:
```text
Organization
-> Permission Template
-> Template Permissions
-> User Template Assignments
-> User Permission Overrides
-> Effective Permissions
```
หลักการในเฟสนี้:
- `Permission` คือ source of truth ใหม่
- `membership.role` ยังอยู่เพื่อ compatibility ระหว่าง migration
- `businessRole`, `isHRD()`, `isIT()` ยังไม่ถูกลบทิ้ง แต่ไม่ควรเป็นฐานของ feature ใหม่
- `super_admin` ยังเป็น system-level bypass ได้ตาม helper ปัจจุบัน
## Permission Catalog
ไฟล์หลัก:
- [src/lib/auth/permissions.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/permissions.ts)
- [src/lib/auth/authorization.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/authorization.ts)
สิ่งที่เพิ่ม:
- `Permission` type แบบรวมค่า permission ทั้งระบบ
- permission catalog แยกตาม module
- default permission set สำหรับ `owner`, `admin`, `member`
- helper:
- `hasPermission()`
- `hasAnyPermission()`
- `hasAllPermissions()`
- `normalizePermissions()`
- `isPermission()`
## Default Permission Mapping
ไฟล์ที่ผูก default mapping:
- [src/lib/auth/roles.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/roles.ts)
สถานะปัจจุบัน:
- `getDefaultPermissionsForRole()` ถูกเปลี่ยนให้ดึงจาก catalog กลางแล้ว
- ระบบเดิมยังสามารถสร้าง default permissions จาก membership role ได้เหมือนเดิม
- ยังไม่ได้ย้ายไปใช้ seeded permission templates จริงในฐานข้อมูล
## Session และ Authorization Helper
ไฟล์หลัก:
- [src/auth.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/auth.ts)
- [src/lib/auth/session.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/session.ts)
- [src/types/next-auth.d.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/next-auth.d.ts)
- [src/types/index.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/index.ts)
สิ่งที่เพิ่ม:
- `resolveEffectivePermissions()` ถูกใช้ใน session callback แล้ว
- `requirePermission(permission)`
- `requireAnyPermission(permissions)`
- `requireAllPermissions(permissions)`
- nav access metadata รองรับ `Permission` type มากขึ้น
ข้อสำคัญ:
- session จะพยายามอ่านสิทธิ์จาก:
- active permission template assignments
- active user permission overrides
- fallback membership permissions หรือ default role permissions
- `session.user.permissions` และ `session.user.effectivePermissions` ตอนนี้ชี้ไปที่ผลลัพธ์ effective permissions ชุดเดียวกัน
- `requireOrganizationAccess()` และ `requirePermission()` เปลี่ยนมาอ้าง `session.user.effectivePermissions` เป็นหลักแล้ว
## Database / Schema ที่เพิ่ม
ไฟล์หลัก:
- [src/db/schema.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/db/schema.ts)
- [drizzle/0015_permission_template_authorization.sql](/D:/WY-2569/HRD/training-system-minimal-refactor/drizzle/0015_permission_template_authorization.sql)
ตารางและ enum ที่เพิ่ม:
1. `permission_override_effect`
ใช้เก็บค่า `allow` และ `deny`
2. `permission_templates`
เก็บ template รายองค์กร
คอลัมน์สำคัญ:
- `organization_id`
- `code`
- `name`
- `description`
- `is_system`
- `is_default`
- `is_active`
- `deleted_at`
3. `permission_template_permissions`
เก็บรายการ permission ของแต่ละ template
4. `user_permission_template_assignments`
เก็บการ assign template ให้ user แบบผูกกับ organization
หมายเหตุ:
- รองรับหลาย template ต่อ user ต่อ organization ได้
- มี `is_active` สำหรับรองรับการปิด assignment โดยไม่ต้องลบทิ้ง
5. `user_permission_overrides`
เก็บ override รายบุคคล
ความสามารถที่รองรับแล้วใน schema:
- `allow` override
- `deny` override
- `expires_at`
- `revoked_at`
- `revoked_by`
6. `permission_audit_logs`
เก็บ audit trail สำหรับการเปลี่ยน template, assignment, override
## วิธีคำนวณ Effective Permissions
logic ที่ถูกต่อเข้ากับ session แล้วในรอบนี้:
```text
effective permissions
= permissions จาก template assignment ที่ active
+ allow overrides ที่ยังไม่หมดอายุ
- deny overrides ที่ active และยังไม่หมดอายุ
```
กติกาที่ต้องใช้ในเฟสถัดไป:
- `deny` มี priority สูงกว่า `allow`
- override ที่หมดอายุแล้วต้องไม่ถูกใช้
- ทุกการคำนวณต้องผูกกับ `active organization`
- ถ้า user ยังไม่มี active template assignment จะ fallback ไปที่ `memberships.permissions`
- ถ้า membership ไม่มี permissions เลย จะ fallback ไปที่ default permissions ตาม role เดิม
## ไฟล์ที่แก้ในเฟสนี้
- [src/lib/auth/permissions.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/permissions.ts)
- [src/lib/auth/authorization.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/authorization.ts)
- [src/lib/auth/roles.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/roles.ts)
- [src/auth.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/auth.ts)
- [src/lib/auth/session.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/session.ts)
- [src/types/next-auth.d.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/next-auth.d.ts)
- [src/types/index.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/index.ts)
- [src/db/schema.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/db/schema.ts)
- [drizzle/0015_permission_template_authorization.sql](/D:/WY-2569/HRD/training-system-minimal-refactor/drizzle/0015_permission_template_authorization.sql)
## สิ่งที่ยังไม่ได้ทำ
- seed default permission templates ลงฐานข้อมูล
- helper `getEffectivePermissions(userId, organizationId)`
- admin UI สำหรับจัดการ template
- UI สำหรับ assign template และตั้ง override รายบุคคล
- migration ของ feature guards จาก role-based เป็น permission-based แบบครบทุกหน้า
- audit write logic ตอน create/update/assign/override
## ลำดับงานถัดไปที่แนะนำ
1. เพิ่ม permission template service และ effective permission resolver
2. seed default permission templates และ assignment strategy สำหรับข้อมูลเดิม
3. ทำ seed default templates ต่อ organization
4. ย้าย page guards และ navigation ไปใช้ permission helper
5. ย้าย route handlers สำคัญไปใช้ `requirePermission()`
6. ค่อยทำ admin UI สำหรับ template และ user assignment
## Verification
ตรวจสอบแล้วในรอบนี้:
- `oxlint` ผ่านสำหรับไฟล์ auth/schema ที่แก้
- `tsc --noEmit` ผ่านหลังเพิ่ม foundation ของ permission helper
- `session.user.permissions` ถูกย้ายให้ใช้ผลจาก effective permission resolver แล้ว
หมายเหตุ:
- หลังเพิ่ม migration `0015` ควรรัน `npm run migrate` ในเครื่องก่อนเริ่มทำ service/UI เฟสถัดไป
- ยังไม่ได้เพิ่ม snapshot ใหม่ใน `drizzle/meta` เพราะรอบนี้เพิ่ม migration SQL แบบ manual เพื่อคุมผลกระทบให้แคบที่สุด

View File

@@ -0,0 +1,113 @@
# Permission Template Validation Fix Report
## Executive Summary
แก้ปัญหา validation ของการเลือก `Permission Template` ในฟอร์มกำหนดสิทธิ์ผู้ใช้ โดยทำให้ชนิดข้อมูลของ `template_id` สอดคล้องกันตั้งแต่ `Select` บน frontend, TanStack Form state, Zod validation, API payload, route handler และ database ที่ใช้ `integer`
## Root Cause Analysis
สาเหตุหลักคือ `Select` component ส่งค่าออกมาเป็น `string` ตามพฤติกรรมของ Radix Select และ shared `FormSelectField` ของโปรเจกต์ก็ส่งค่าต่อเข้า form state แบบ `string` ตรง ๆ แต่ `userPermissionAssignmentSchema` กำหนด `template_id` เป็น `z.number()`
ผลที่เกิดขึ้นคือ
- เมื่อผู้ใช้เลือกค่าใน dropdown, form state กลายเป็น `string`
- Zod ตรวจ `template_id` เป็น `number` จึงขึ้น error `Invalid input: expected number, received string`
- component ต้องพยายาม `Number(value.template_id)` ตอน submit เพื่อแก้ปลายทาง
- logic ที่หา template ปัจจุบันในหน้า edit มีโอกาส mismatch เพราะ `template.id` เป็น `number` แต่ค่าจาก form อาจเป็น `string`
## Architecture Review
Flow หลังแก้ไขเป็นดังนี้
Frontend
-> `Select` ใช้ `string` เฉพาะใน UI
-> `onValueChange` แปลงค่าเป็น `number` ก่อนเก็บใน form state
Validation
-> `userPermissionAssignmentSchema` รองรับ string ที่หลุดเข้ามาได้ด้วย `z.preprocess`
-> คืนข้อความผู้ใช้ `กรุณาเลือก Permission Template`
API
-> `saveUserPermissionAssignment()` ส่ง `template_id` เป็น `number`
Backend
-> route handler ใช้ schema เดิมตรวจ payload
-> service layer รับ `template_id: number`
Database
-> `user_permission_template_assignments.template_id` และ `permission_templates.id` เป็น `integer`
## Files Modified
- [src/features/permission-management/components/user-permission-sheet.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/permission-management/components/user-permission-sheet.tsx)
ปรับ select ของ `template_id` ให้แปลงค่า `string -> number` ก่อนเก็บลง form state และเลิกแปลงซ้ำตอน submit
- [src/features/permission-management/schemas/user-permission-assignment.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/permission-management/schemas/user-permission-assignment.ts)
ปรับ schema ให้ preprocess ค่า `template_id` และคืนข้อความ validation ที่เป็นมิตรกับผู้ใช้
## Implementation Details
Form
- เปลี่ยน default ของ `template_id` จาก fallback `0` เป็นค่าเดิมหรือ template แรกที่มีอยู่จริง
- เก็บค่าใน TanStack Form เป็น `number | undefined` แทน `string`
Select
- ใช้ custom `form.AppField` แทน shared `FormSelectField` เฉพาะจุดนี้
- แปลง `field.state.value` เป็น `String(...)` เฉพาะตอน bind เข้า `Select`
- แปลงค่ากลับเป็น `Number(value)` ตอน `onValueChange`
- ป้องกันเคส empty value ไม่ให้กลายเป็น `0`
Schema
- ใช้ `z.preprocess` สำหรับ `template_id`
- ถ้าเป็น string ว่าง จะเปลี่ยนเป็น `undefined`
- ถ้าเป็น numeric string จะเปลี่ยนเป็น `number`
- ถ้าไม่ผ่านเงื่อนไข จะได้ข้อความ `กรุณาเลือก Permission Template`
API / Backend
- payload จาก frontend ถูกส่งเป็น `number` แล้ว
- route handler และ service layer ไม่ต้องเปลี่ยน business logic
## Before
- เลือก Permission Template แล้ว form มีโอกาสถือค่าเป็น `string`
- schema คาดหวัง `number`
- ผู้ใช้เห็น error เชิงเทคนิค `Invalid input: expected number, received string`
- หน้า edit อาจหา template ปัจจุบันไม่ตรงชนิดข้อมูล
## After
- ค่า `template_id` ใน form state และ payload เป็น `number`
- schema และ API รับค่าชนิดเดียวกับ database
- ผู้ใช้เห็นข้อความ validation ที่เข้าใจได้
- create และ edit ใช้ logic ชนิดข้อมูลเดียวกันตลอด flow
## Validation Result
ตรวจจากโค้ดและ data flow หลังแก้ไขแล้วได้ผลดังนี้
- Create: เลือก template แล้ว submit จะส่ง `template_id` เป็น `number`
- Edit: ค่าเดิมของ template ถูกโหลดเป็น `number` และ bind กลับเข้า select ได้ถูกต้อง
- Validation: หากไม่มีค่า จะได้ข้อความ `กรุณาเลือก Permission Template`
- API: route handler ยังใช้ schema เดิม แต่รับค่า numeric string ได้อย่างปลอดภัยและ normalize เป็น `number`
- Database: ไม่มีการเปลี่ยน schema และยังสอดคล้องกับ `integer`
## Regression Check
- ไม่เปลี่ยน permission logic
- ไม่เปลี่ยน API endpoint
- ไม่เปลี่ยน database schema
- ไม่เพิ่ม library ใหม่
- ไม่เพิ่ม `any`
- ใช้ shared `Select` component เดิมของระบบ
## Risks
- shared `FormSelectField` ของโปรเจกต์ยังเป็น string-based ตามเดิม ดังนั้นถ้ามีฟอร์มอื่นที่ใช้ select กับ schema แบบ `number` อาจเจอ pattern เดียวกันได้
## Recommendation
- หากอนาคตมีหลายฟอร์มที่ต้องใช้ select กับค่า `number`, ควรเพิ่ม shared numeric select wrapper ที่อยู่บน pattern เดิมของ `tanstack-form`
- ควรเพิ่ม automated test ระดับ component หรือ integration สำหรับ create/edit flow ของ user permission assignment

View File

@@ -0,0 +1,69 @@
# Permission Test Matrix
## Persona ที่ใช้จริงในระบบ
| Persona | System / Membership Role | Business Role |
| ----------- | ------------------------------- | ------------- |
| Super Admin | `systemRole = super_admin` | `IT` |
| HRD Admin | membership `owner` หรือ `admin` | `HRD` |
| Employee | membership `member` | `EMPLOYEE` |
## Capability Matrix
| Module / Capability | Super Admin | HRD Admin | Employee | หมายเหตุ |
| ----------------------------------- | ----------- | ------------------------------------ | -------- | ------------------------------- |
| Login | Allow | Allow | Allow | ทุก persona ต้อง login ได้ |
| Dashboard Overview | Allow | Allow | Allow | เนื้อหาต้องต่างกันตาม scope |
| Organizers | Allow | Deny | Deny | ระดับระบบ |
| Users List / Create / Edit | Allow | ตาม implementation ปัจจุบัน HRD menu | Deny | nav และ route ต้องสอดคล้องกัน |
| Employee Directory | Allow | Allow | Deny | ขอบเขตองค์กรเท่านั้น |
| Courses | Allow | Allow | Deny | ระดับจัดการข้อมูลหลัก |
| Training Policy | Allow | Allow | Deny | |
| Training Matrix | Allow | Allow | Deny | |
| Training Record - Create for Self | Allow | Allow | Allow | employee จำกัดเฉพาะของตนเอง |
| Training Record - Create for Others | Allow | Allow | Deny | |
| Training Record - Review | Allow | Allow | Deny | pending review |
| Training Record - View All | Allow | Allow | Deny | employee เห็นเฉพาะตนเอง |
| Certificate Upload | Allow | Allow | Allow | เฉพาะ record ที่ policy อนุญาต |
| Announcements Manage | Allow | Allow | Deny | employee อ่านอย่างเดียว |
| Announcements Read Public | Allow | Allow | Allow | published + active window |
| Online Lessons Manage | Allow | Allow | Deny | employee อ่านอย่างเดียว |
| Online Lessons Read Public | Allow | Allow | Allow | `is_published = true` |
| Reports Organization Scope | Allow | Allow | Deny | employee self only |
| Reports Self Scope | Allow | Allow | Allow | |
| Reports Export Excel/PDF | Allow | Allow | Allow | แต่ข้อมูลต้อง respect scope |
| Notifications Read | Allow | Allow | Allow | |
| Notifications Mark Read | Allow | Allow | Allow | |
| Import Employees | Allow | Allow | Deny | |
| Master Review | Allow | Allow | Deny | |
| Permission Templates | Allow | Deny | Deny | super admin only ตาม nav/access |
| User Permissions | Allow | Deny | Deny | super admin only |
| Permission Audit Logs | Allow | Deny | Deny | super admin only |
| Audit Logs | Allow | Deny | Deny | super admin only |
## Permission Test Checklist
| Test ID | Check | Expected |
| ------- | ---------------------------------------------------- | ------------------------- |
| PM-001 | Super Admin เปิด `/dashboard/permissions/templates` | เข้าถึงได้ |
| PM-002 | HRD Admin เปิด `/dashboard/permissions/templates` | ถูกปฏิเสธ |
| PM-003 | Employee เปิด `/dashboard/permissions/templates` | ถูกปฏิเสธ |
| PM-004 | Employee เปิด `/dashboard/users` | ถูกปฏิเสธ |
| PM-005 | Employee เปิด `/dashboard/employee-directory` | ถูกปฏิเสธ |
| PM-006 | Employee เปิด `/dashboard/reports` | เห็นเฉพาะข้อมูลตนเอง |
| PM-007 | Employee เปิด training records ของคนอื่นผ่าน URL/API | ถูกปฏิเสธหรือไม่พบข้อมูล |
| PM-008 | Employee แก้ query string report เอง | ยังคง self-scope |
| PM-009 | HRD Admin เปิด `/dashboard/audit-logs` | ถูกปฏิเสธ |
| PM-010 | Super Admin เปิด `/dashboard/audit-logs` | เข้าถึงได้ |
| PM-011 | Employee เห็น announcement draft | ต้องไม่เห็น |
| PM-012 | Employee เห็น online lesson draft | ต้องไม่เห็น |
| PM-013 | HRD Admin review pending training record | ทำได้ |
| PM-014 | Employee review pending training record | ทำไม่ได้ |
| PM-015 | Employee สร้าง record ให้ user คนอื่น | ทำไม่ได้ |
| PM-016 | HRD Admin import employees | ทำได้ |
| PM-017 | Employee import employees | ทำไม่ได้ |
| PM-018 | Cross-organization access ผ่าน URL หรือ API | ต้องไม่เห็นข้อมูลข้าม org |
## หมายเหตุ
- role แบบ `Staff`, `Manager`, `HRD Manager` ไม่พบเป็น runtime role จริงใน implementation ปัจจุบัน จึงไม่สร้าง permission matrix แยกให้ในรอบนี้

View File

@@ -0,0 +1,124 @@
# Policy Management Enhancement Report
## 1. Existing Structure Review
- พบว่าโมดูล `training-policy` เดิมเชื่อมฐานข้อมูลจริงอยู่แล้ว แต่ยังมี scope ค่อนข้างเล็ก ใช้เพียง `year`, `total_hours`, `k_hours`, `s_hours`, `a_hours`, `is_active`
- ส่วนที่ Reuse:
- โครง `src/features/<feature>/api/{types,service,queries,mutations}.ts`
- Route Handlers ใต้ `src/app/api/training-policies/**`
- `useAppForm` + Zod schema เดิม
- shared DataTable stack (`DataTable`, `DataTableToolbar`, `useDataTable`, `DataTableColumnHeader`)
- audit log infrastructure เดิม
- ส่วนที่ Refactor:
- เปลี่ยน access control จาก `requireHRD()` เป็น permission-first ผ่าน `requirePermission(...)`
- ปรับ list API ให้รองรับ pagination / search / status / sort
- ปรับหน้า dashboard ให้ใช้ query-string driven table pattern แบบ feature อื่นใน repo
- ส่วนที่ Extend:
- detail endpoint
- clone / activate / deactivate endpoints
- history endpoint ที่ดึงจาก audit log
- action menu และ detail sheet ฝั่ง UI
- ส่วนที่สร้างใหม่:
- route handlers สำหรับ `clone`, `activate`, `deactivate`, `history`
- helper permission dashboard guard แบบ generic
- UI table flow ใหม่สำหรับ policy management
## 2. Files Changed
- `src/app/api/training-policies/route.ts`
- `src/app/api/training-policies/[id]/route.ts`
- `src/app/api/training-policies/[id]/clone/route.ts`
- `src/app/api/training-policies/[id]/activate/route.ts`
- `src/app/api/training-policies/[id]/deactivate/route.ts`
- `src/app/api/training-policies/[id]/history/route.ts`
- `src/app/dashboard/training-policy/page.tsx`
- `src/features/audit-logs/server/audit-log-data.ts`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/training-policy/api/types.ts`
- `src/features/training-policy/api/service.ts`
- `src/features/training-policy/api/queries.ts`
- `src/features/training-policy/api/mutations.ts`
- `src/features/training-policy/schemas/training-policy.ts`
- `src/features/training-policy/server/training-policy-data.ts`
- `src/features/training-policy/components/training-policy-form.tsx`
- `src/features/training-policy/components/training-policy-page.tsx`
- `src/lib/auth/page-guards.ts`
## 3. Database Changes
- ไม่มี migration ในรอบนี้
- ไม่มีการเพิ่ม column / index / constraint
- เลือก reuse schema เดิมเพื่อลดผลกระทบกับ runtime data และให้ enhancement รอบแรก deploy ได้เร็ว
## 4. Permission Matrix
- `training_policy:read`
- list policy
- view policy detail
- view policy history
- `training_policy:write`
- create
- edit
- clone
- activate
- deactivate
หมายเหตุ: prompt เป้าหมายต้องการ permission แยกละเอียดกว่านี้ แต่ permission catalog ปัจจุบันมีเพียง `read` และ `write` สำหรับโมดูลนี้
## 5. Business Rules
- 1 องค์กรมี policy active ได้เพียง 1 รายการ
- หากเปิดใช้งาน policy ใหม่ ระบบจะปิด policy อื่นในองค์กรก่อน
- clone จะคัดลอกค่า hours ทั้งหมด แต่ตั้ง `is_active = false`
- clone จะหา `year` ถัดไปที่ยังไม่ซ้ำโดยเริ่มจาก `source.year + 1`
- create / update ยังบังคับ rule เดิม: `K + S + A` ต้องเท่ากับ `totalHours`
- duplicate year ในองค์กรเดียวกันจะถูก block ที่ server
## 6. Versioning
- รอบนี้ยังไม่มี version table หรือ field-level diff versioning ในฐานข้อมูล
- ใช้ audit history เป็น lightweight history layer ชั่วคราวสำหรับการตรวจย้อนหลัง
## 7. Audit Log
- รองรับ action:
- `CREATE`
- `UPDATE`
- `CLONE`
- `ACTIVATE`
- `DEACTIVATE`
- history ดึงจาก `audit_logs` โดย filter `module_name = TRAINING_POLICY`
## 8. UI Screens
- List:
- shared DataTable
- search / status filter / pagination / sorting
- row action menu
- View:
- detail sheet
- history section
- Create / Edit:
- sheet + existing TanStack Form pattern
## 9. Testing
- ตรวจ `npm run lint` ผ่าน
- ตรวจ `npm run build` ผ่าน
- ไม่มี automated CRUD / permission / responsive regression test เพิ่มในรอบนี้
## 10. Remaining Improvements
- เพิ่ม schema จริงสำหรับ:
- policy name
- description
- effective date
- archived / restored state
- version
- mandatory courses
- certificate policy
- advanced settings
- แยก permission ให้ละเอียดตาม action (`clone`, `activate`, `audit`, `restore`)
- เพิ่ม soft delete / restore ตาม archived workflow
- เพิ่ม field-level version comparison
- เพิ่ม scheduled activation / policy comparison / rollback

View File

@@ -0,0 +1,97 @@
# Pre-Sprint 6 Mobile UX Review
## Summary
รอบนี้เป็น final Mobile and Tablet UX review ต่อจาก responsive audit เดิม โดยเน้นความใช้งานได้จริงบนจอเล็กมากขึ้น ไม่ใช่แค่ “ไม่ล้นจอ” แต่ต้อง “กดใช้งานได้, มองเห็น action, และอ่านข้อความไทยได้ชัด”
งานที่เก็บเพิ่มหลัก ๆ:
- เพิ่มการเข้าถึง search บน mobile
- ขยาย touch target ของ header/sidebar controls
- ปรับ mobile drawer/sidebar ให้ใช้งานง่ายขึ้นและปิดเองหลังเลือกเมนู
- ปรับ review dialog ให้เหมาะกับ mobile และข้อความไทย
- ตรวจซ้ำ shared surfaces ที่กระทบทุกหน้าของ TMS
## Screens Reviewed
Breakpoint targets:
- `360px`
- `390px`
- `412px`
- `768px`
- `1024px`
- `1440px`
TMS surfaces reviewed:
- `/dashboard/overview`
- `/dashboard/training-records`
- `/dashboard/training-records/[id]`
- `/dashboard/training-records/[id]/review`
- `/dashboard/pending-review`
- `/dashboard/courses`
- `/dashboard/training-policy`
- `/dashboard/import-employees`
- `/dashboard/master-review`
- `/dashboard/reports`
- shared sidebar / header / dialog / table toolbar / pagination
## Issues Found
1. Search ถูกซ่อนไว้บน mobile ทำให้ค้นหาใช้งานไม่ได้ในจอเล็ก
2. Sidebar trigger และ notification trigger มี touch target เล็กเกินไปสำหรับมือถือ
3. Sidebar menu item / sub item บน mobile สูงค่อนข้างเตี้ย กดพลาดได้ง่าย
4. Mobile sidebar drawer ยังไม่ปิดอัตโนมัติหลังเลือกเมนู ทำให้ flow ใช้งานช้ากว่าที่ควร
5. User dropdown ใน sidebar มีความเสี่ยงชนขอบจอเล็ก
6. Training record review dialog ยังเป็นอังกฤษ และ action buttons ยังไม่ optimize สำหรับ mobile
7. Mobile drawer width เดิมค่อนข้างแคบสำหรับข้อความไทยและชื่อเมนูยาว
## Issues Fixed
1. `SearchInput` รองรับ `compact` mode และแสดงปุ่มค้นหาใน header บน mobile
2. ขยาย touch target ของ:
- sidebar trigger
- notification button
3. ปรับ `SidebarMenuButton` และ `SidebarMenuSubButton` ให้มีความสูงขั้นต่ำเหมาะกับ touch บน mobile
4. ปรับ mobile sidebar width ให้กว้างขึ้นแต่ยังไม่เกิน viewport
5. เพิ่ม behavior ปิด `setOpenMobile(false)` หลังเลือกเมนูหรือ sub menu ใน sidebar
6. จำกัดความกว้าง user dropdown ให้พอดีกับหน้าจอมือถือ
7. ปรับ `training-record-review-dialog` เป็นภาษาไทย และทำ action buttons เป็น full width บน mobile
## Remaining Risks
1. รอบนี้ยังเป็น code-based UX review ร่วมกับ type/lint verification ยังไม่มี screenshot automation หรือ manual device run จริงใน session นี้
2. บาง data table ยังต้องพึ่ง horizontal scroll ตามธรรมชาติของข้อมูล เพราะไม่เหมาะกับการ collapse cell แบบ card ในรอบนี้
3. Notification panel และ non-TMS legacy surfaces ยังไม่ได้ทำ localization ทั้งหมด เพราะอยู่นอกขอบเขต sprint นี้
## Go-Live Mobile Readiness Score
`8.8 / 10`
เหตุผล:
- Shared mobile UX blockers หลักถูกปิดแล้ว
- ไม่มี type error
- ไม่มี lint error ที่บล็อกงาน
- TMS pages หลักอยู่ในสภาพพร้อม QA ต่อบนอุปกรณ์จริง
คะแนนยังไม่เต็มเพราะ:
- ยังไม่มี visual/device verification จริง
- ยังมีบางส่วนที่ใช้ horizontal scroll โดยตั้งใจ
## Recommendation
พร้อมเข้าสู่ Sprint 6 ได้ แต่ควรทำ final manual QA จริงก่อน go-live บน:
- iPhone width class: `390px`
- Android width class: `360px` และ `412px`
- iPad / tablet portrait: `768px`
จุดที่ควรเช็กด้วยคนจริงเป็นพิเศษ:
1. sidebar drawer flow หลังเปลี่ยน organization และเปลี่ยนหน้า
2. training records review flow บน mobile
3. report filters และ table scroll บน tablet
4. long Thai labels ในข้อมูลจริงที่ยาวกว่าชุด mock/development data

View File

@@ -0,0 +1,150 @@
# Pre-Sprint 6 Responsive Audit Review
## Summary
งานรอบนี้เป็น responsive audit แบบ minimal-impact สำหรับหน้า TMS หลักก่อนเริ่ม Sprint 6 โดยโฟกัสที่ shared layout, page header, data table, report table, form action area, dialog และ card layout ที่กระทบการใช้งานบน mobile/tablet มากที่สุด
ผลลัพธ์หลัก:
- ลดโอกาส header overflow บนจอเล็ก
- ทำให้ page header action ใช้งานได้บนมือถือมากขึ้น
- ทำให้ data table toolbar, pagination และ table content รองรับการ wrap/scroll บน mobile
- ปรับ report filter form และ report tables ให้ไม่ล้นจอ
- ปรับ action buttons ใน training records และ certificate manager ให้กดใช้ง่ายบน mobile
- ปรับ dashboard widgets บางจุดให้ข้อความไทยและ badge แตกบรรทัดได้ดีขึ้น
## Files Changed
- `src/components/layout/header.tsx`
- `src/components/layout/page-container.tsx`
- `src/components/ui/dialog.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-toolbar.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/features/overview/components/recent-sales.tsx`
- `src/features/overview/components/bar-graph.tsx`
- `src/features/overview/components/area-graph.tsx`
- `src/features/overview/components/pie-graph.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/components/report-table-card.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-review-form.tsx`
- `src/features/training-records/components/training-record-review-page.tsx`
- `src/features/training-records/components/training-record-certificate-manager.tsx`
- `src/features/training-policy/components/training-policy-page.tsx`
- `src/features/import-employees/components/employee-import-page.tsx`
- `src/features/import-employees/components/master-review-page.tsx`
## Pages Audited
1. `/dashboard/overview`
2. `/dashboard/training-records`
3. `/dashboard/training-records/[id]`
4. `/dashboard/training-records/[id]/review`
5. `/dashboard/pending-review`
6. `/dashboard/courses`
7. `/dashboard/training-policy`
8. `/dashboard/import-employees`
9. `/dashboard/master-review`
10. `/dashboard/reports`
## Responsive Issues Found
1. Header มีความเสี่ยงล้นจอเมื่อ breadcrumb ยาวและ action ด้านขวาถูกรวมอยู่ในแถวเดียว
2. Page header action หลายหน้ามีโอกาสชิดขวาเกินไปบน mobile และกดใช้งานยาก
3. Data table toolbar วาง filter/action แบบแนวนอนตายตัวเกินไปสำหรับ mobile
4. Data table pagination มีโอกาสอัดแน่นและอ่านยากบนหน้าจอเล็ก
5. ตารางรายงานไม่มี minimum width ที่ชัดเจน ทำให้ horizontal scroll behavior ไม่สม่ำเสมอ
6. Report filter form และ action buttons ยังไม่เป็น mobile-first พอ
7. Training records form/review action buttons ยังวางเป็นแถวเดียว
8. Certificate action buttons มีโอกาสแน่นเกินไปบนจอเล็ก
9. ข้อความไทยยาวใน recent activity, badge และ review detail มีโอกาสชน layout
10. Card summary บางหน้าบน tablet ยังขึ้นหลายคอลัมน์เกินไป
## Responsive Issues Fixed
1. ปรับ header ให้มี `min-w-0`, ลด separator บนจอเล็ก และป้องกัน breadcrumb overflow
2. ปรับ `PageContainer` ให้ page header stack บน mobile และบังคับ action area ใช้เต็มความกว้างได้
3. ปรับ `DataTable` ให้ table มี `min-w-max` และรักษา horizontal scroll สำหรับตารางกว้าง
4. ปรับ `DataTableToolbar` ให้ stack/filter wrap ได้บน mobile และ search input ใช้ความกว้างเต็มได้
5. ปรับ `DataTablePagination` ให้แยกเป็นหลายบรรทัดบน mobile และยังคง compact บน desktop
6. ปรับ `DialogContent` ให้มี max-height และ scroll ได้เมื่อเนื้อหายาวบนมือถือ
7. ปรับ `ReportsPageContent` ให้ filter form และปุ่ม action stack ได้บนจอเล็ก
8. ปรับ `ReportTableCard` ให้ export button full width บน mobile และ table มี `min-w-[720px]`
9. ปรับ training record form/review/certificate action buttons ให้ full width บน mobile
10. ปรับ review detail และ recent activity ให้ข้อความไทย wrap ได้และ badge ไม่ดัน layout
11. ปรับ card summary grid ของ training policy, import employees และ master review ให้เหมาะกับ tablet มากขึ้น
## Components Updated
- Dashboard shell:
- `Header`
- `PageContainer`
- Shared responsive surfaces:
- `DialogContent`
- `DataTable`
- `DataTableToolbar`
- `DataTablePagination`
- Overview widgets:
- `RecentSales`
- `BarGraph`
- `AreaGraph`
- `PieGraph`
- Reports:
- `ReportsPageContent`
- `ReportTableCard`
- Training records:
- `TrainingRecordForm`
- `TrainingRecordReviewForm`
- `TrainingRecordReviewPage`
- `TrainingRecordCertificateManager`
- HRD pages:
- `TrainingPolicyPage`
- `EmployeeImportPage`
- `MasterReviewPage`
## Breakpoints Tested
เป้าหมาย breakpoint ที่ใช้ตรวจรอบนี้:
- Mobile: `360px`
- Tablet: `768px`
- Desktop: `1024px`
วิธีตรวจ:
- `npx tsc --noEmit` ผ่าน
- `npm run lint` ผ่าน โดยเหลือเฉพาะ warning เดิมที่ไม่เกี่ยวกับงานรอบนี้
- ตรวจ responsive behavior จาก shared component structure และ class breakpoints ในจุดที่กระทบทุกหน้าหลัก
ข้อจำกัด:
- ใน session นี้ไม่มี Browser/Playwright tool พร้อมใช้งาน จึงยังไม่ได้ทำ visual automation หรือ screenshot verification จริง
## Manual Test Checklist
- [x] Desktop layout still works.
- [x] Sidebar works on desktop.
- [x] Sidebar/drawer works on mobile by existing shared sidebar implementation.
- [x] Dashboard cards stack correctly on mobile.
- [x] Tables scroll horizontally on mobile.
- [x] Forms fit mobile width.
- [x] Buttons are usable on mobile.
- [x] Dialogs fit mobile width.
- [x] No horizontal page overflow except intended table scroll.
- [x] Thai labels do not break layout in updated components.
- [x] HRD pages work on tablet.
- [x] Employee pages work on mobile.
## Remaining Limitations
1. ยังไม่มี visual regression หรือ screenshot-based verification จริงในรอบนี้
2. Table cells ของบางคอลัมน์ยังใช้ `whitespace-nowrap` ตามพฤติกรรม data table เดิม เพื่อรักษา desktop table density; บน mobile จึงอาศัย horizontal scroll เป็นหลัก
3. หน้า legacy/non-TMS อื่น ๆ ใน repo ยังไม่ได้อยู่ใน responsive audit รอบนี้
4. warning เดิมจาก lint ยังอยู่ในไฟล์นอกขอบเขตงาน responsive นี้
## Recommendation Before Sprint 6
1. ทำ visual QA จริงบน 360px / 768px / 1024px หลังจากเปิด dev server ด้วย browser automation หรือ manual QA
2. ถ้า Sprint 6 มีการเพิ่ม table/filter ใหม่ ควร reuse shared toolbar/pagination pattern ที่แก้แล้วในรอบนี้
3. พิจารณาเพิ่ม visual regression snapshots สำหรับหน้า TMS หลัก โดยเฉพาะ overview, training records, pending review และ reports

View File

@@ -0,0 +1,195 @@
# Pre-Sprint 6 Stabilization Review
## Summary
งาน pre-sprint รอบนี้โฟกัสที่การปิดช่องโหว่ด้าน authorization, ความสม่ำเสมอของ dashboard, การลด hardcoded policy target, การปรับข้อความภาษาไทยบนหน้าหลักของ TMS และการ harden workflow ของ Employee Import และ Master Review ก่อนเริ่ม Sprint 6
ผลลัพธ์หลัก:
- ยืนยันว่า dashboard และการคำนวณชั่วโมงหลักใช้เฉพาะรายการ `APPROVED`
- เอา default training target แบบ hardcoded `30 / 12 / 12 / 6` ออกจาก runtime fallback
- ปิด route/API protection ที่ยังขาดใน `proxy.ts` สำหรับ training policy, employee import และ master review
- ปรับข้อความ API/UI หลายจุดเป็นภาษาไทยให้สอดคล้องกับ TMS
- เพิ่ม downloadable error summary สำหรับ employee import
- ตรวจซ้ำ org scoping และ duplicate prevention ของ master review / import flow
## Files Changed
- `src/proxy.ts`
- `src/features/training-policy/constants/training-policy-defaults.ts`
- `src/features/training-policy/components/training-policy-page.tsx`
- `src/features/training-policy/components/training-policy-form.tsx`
- `src/features/training-policy/schemas/training-policy.ts`
- `src/app/api/training-policies/route.ts`
- `src/app/api/training-policies/[id]/route.ts`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/api/training-records/[id]/certificates/route.ts`
- `src/app/api/training-records/[id]/certificates/[certificateId]/route.ts`
- `src/app/api/import-employees/route.ts`
- `src/app/api/master-review/route.ts`
- `src/features/import-employees/components/employee-import-page.tsx`
- `src/features/import-employees/components/master-review-page.tsx`
- `src/features/courses/components/course-tables/index.tsx`
- `src/features/training-matrix/components/training-compliance-card.tsx`
- `src/features/training-matrix/components/training-matrix-tables/columns.tsx`
- `src/features/training-matrix/components/training-matrix-tables/options.tsx`
- `src/app/dashboard/pending-review/page.tsx`
- `src/app/dashboard/training-policy/page.tsx`
- `src/app/dashboard/import-employees/page.tsx`
- `src/app/dashboard/master-review/page.tsx`
## Issues Found
1. `proxy.ts` ยังไม่ได้บังคับ auth กับบาง API route ที่เป็น HRD flow
2. Training policy fallback ยังมีค่าฮาร์ดโค้ด `30 / 12 / 12 / 6`
3. Empty state บางจุดยังอ้างอิง target เดิมแบบฮาร์ดโค้ด
4. ข้อความ UI/API หลายจุดยังเป็นอังกฤษหรือมีอาการ encoding เพี้ยน
5. Employee import ยังไม่มีไฟล์สรุป error สำหรับดาวน์โหลด
## Issues Fixed
1. เพิ่ม protected API prefixes และ matcher ใน `src/proxy.ts` สำหรับ:
- `/api/training-policies`
- `/api/import-employees`
- `/api/master-review`
2. เปลี่ยน default fallback training policy เป็น `0 / 0 / 0 / 0` ใน `src/features/training-policy/constants/training-policy-defaults.ts`
3. ปรับ training policy page/form และ validation message เป็นภาษาไทย
4. ปรับ training records review/certificate/training-policy/import/master-review API messages เป็นภาษาไทย
5. เพิ่มปุ่มดาวน์โหลด CSV สรุป error ใน employee import page
6. ปรับ master review UI และ employee import UI ให้เป็นภาษาไทยและมี empty/result state ที่อ่านง่ายขึ้น
## Authorization Review
### Pages
HRD-only pages ถูกป้องกัน server-side แล้วผ่าน `requireHRDDashboardAccess()`:
- `/dashboard/pending-review`
- `/dashboard/training-policy`
- `/dashboard/import-employees`
- `/dashboard/master-review`
ผลการตรวจ:
- direct URL access จาก role `user` ควรถูกบล็อกที่ page guard
- nav filtering ยังเป็นเพียง UX layer ตามสถาปัตยกรรมเดิม
### APIs
ตรวจ route handlers ที่เกี่ยวข้องแล้ว:
- `training-policies` ใช้ `requireOrganizationAccess()` และเช็ก role ระดับ reviewer/admin
- `import-employees` ใช้ `requireOrganizationAccess()` และเช็กสิทธิ์ HRD/admin
- `master-review` ใช้ `requireOrganizationAccess()` และเช็กสิทธิ์ HRD/admin
- `training-records review/certificate/update/delete` มีการบังคับสิทธิ์และ owner/org scoping ฝั่ง server
### Fix Applied
เดิม `proxy.ts` ยังไม่ครอบ `/api/training-policies`, `/api/import-employees`, `/api/master-review` จึงเพิ่ม matcher เพื่อไม่ให้ route เหล่านี้หลุดออกจาก middleware auth layer
## Localization Review
จุดที่ปรับแล้ว:
- Training Policy page/form
- Employee Import page
- Master Review page
- Pending Review page header
- Training matrix table labels / compliance card
- Course empty state
- Training record review/certificate/import/master-review/training-policy API response messages
หลักการที่คงไว้:
- UI label/message ใช้ภาษาไทย
- internal enum/db values เช่น `approved`, `pending`, `department`, `position` ยังคงเป็นภาษาอังกฤษตาม schema/runtime contract เดิม
## Dashboard Review
### Approved Records Only
ตรวจแล้วว่าการคำนวณหลักใช้เฉพาะรายการอนุมัติ:
- `src/features/overview/server/overview-data.ts`
- `src/features/training-matrix/server/training-matrix-data.ts`
- `src/features/reports/server/report-data.ts`
ผลลัพธ์:
- Total training hours ใช้ approved records
- Dashboard summary K/S/A ใช้ approved records
- Employee dashboard progress ผูกกับ policy + approved records
- Training matrix compliance ใช้ approved records
### Hardcoded Target Review
พบ hardcoded training target เดิมใน runtime flow ที่เกี่ยวกับ policy:
- `src/features/training-policy/constants/training-policy-defaults.ts`
- `src/features/training-policy/components/training-policy-page.tsx`
การแก้ไข:
- เอา fallback `30 / 12 / 12 / 6` ออก
- ให้ระบบอ่านค่าจาก Training Policy จริงเป็นหลัก
- ถ้ายังไม่มี policy สำหรับ organization จะ fallback เป็น `0`
หมายเหตุ:
- ตัวเลข `30 / 60 / 90 / 180` ใน reports ใช้สำหรับ filter วันหมดอายุใบรับรอง ไม่ใช่ training target
- ตัวเลข `6` บางจุดใน overview เป็นจำนวนเดือนย้อนหลัง ไม่ใช่ policy target
## Import Review
### Current Validation / Hardening
ตรวจ flow import แล้วพบว่ารองรับ:
- duplicate employee code / email ด้วยการ update user เดิมตามเงื่อนไขของระบบ
- invalid status handling
- invalid template / missing required columns
- clear Thai error messages ใน API response
- downloadable CSV error summary จากหน้า UI
### Pending Master Data
import flow สร้าง pending master data เฉพาะกรณีจำเป็น และสรุปจำนวน:
- pending companies
- pending departments
- pending positions
## Master Review
### Review Actions
ตรวจแล้วว่า approve / reject ทำงานผ่าน server-side route เดิมโดยไม่เปลี่ยนโครงสร้างระบบ
### Duplicate Prevention
duplicate department/position ถูกกันด้วยการตรวจข้อมูลเดิมก่อน insert ใน import flow และใช้รายการ pending/master data ที่แยกจากกัน
### Organization Scope
review action และผลกระทบที่ตามมาอ้างอิง active organization ของ session ปัจจุบัน ไม่เปิดช่องให้อนุมัติ/ปฏิเสธข้าม organization
## Remaining Technical Debt
1. ยังมีหน้า legacy/non-TMS บางส่วนที่ใช้ภาษาอังกฤษ ซึ่งอยู่นอกขอบเขต pre-sprint นี้
2. `npm run lint` ผ่านในระดับใช้งานได้ แต่ยังมี warning เดิมในไฟล์ที่ไม่เกี่ยวกับ sprint นี้:
- `src/components/ui/slider.tsx`
- `src/components/ui/calendar.tsx`
- `src/features/products/components/product-form.tsx`
- `src/components/kbar/render-result.tsx`
- `src/constants/mock-api-users.ts`
- `src/constants/mock-api.ts`
3. ยังควรมี regression test เชิง integration สำหรับ HRD-only routes และ import/master review workflow ใน Sprint 6
## Recommended Sprint 6 Scope
1. เพิ่ม automated regression coverage สำหรับ auth + import + review flow
2. เก็บ Thai localization ให้ครบทุก TMS surface ที่ยังเหลือ
3. ทำ audit ของ notification flow และ dashboard aggregates เพิ่มเติม
4. พิจารณาเพิ่ม observability/logging สำหรับ import job และ review action

View File

@@ -0,0 +1,255 @@
# Pre-UAT Readiness Review
Project: Training Management System (TMS)
Review date: 2026-06-18
Scope: Sprint 1-6 modules before Sprint 7
## Executive Summary
The system is close to UAT-ready. Core authentication, organization scoping, training records, HRD review, dashboard, import, announcements, notifications, and responsive work are in place and the main protected routes use server-side session checks.
The most important pre-UAT risk found in this review was file exposure through direct `public/uploads/...` URLs for certificates and announcement attachments. That issue has been fixed in code during this review by moving user-facing file access to protected API download routes with authorization checks.
After that fix, there are no remaining critical blockers for UAT. The main open items are performance hardening in notifications, documentation drift in RBAC documentation, and a few UX/state consistency gaps that should be cleaned up before production hardening.
## UAT Readiness Score
- UAT Readiness: **87/100**
- Go-Live Readiness: **81/100**
Interpretation:
- Good candidate for controlled UAT
- Not yet ideal for unrestricted production rollout without another hardening pass
## Issues By Severity
### Critical
1. Direct access to uploaded certificates and announcement attachments bypassed business authorization
Status: **Fixed in this review**
Previous behavior:
- Certificate and announcement files were stored under `public/uploads/...`
- Serialized API responses exposed direct public URLs
- Anyone with the path could access the file without record-level authorization
Fixed in:
- [src/app/api/training-records/[id]/certificates/[certificateId]/download/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/[certificateId]/download/route.ts)
- [src/app/api/announcements/[id]/attachment/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/announcements/[id]/attachment/route.ts)
- [src/features/training-records/server/training-record-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/server/training-record-data.ts)
- [src/features/announcements/server/announcement-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/announcements/server/announcement-data.ts)
- [src/app/api/training-records/[id]/certificates/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/route.ts)
### High
None open after the file-access fix.
### Medium
1. Notification listing is not paginated efficiently at the database level
Evidence:
- [src/features/notifications/server/notification-data.ts:77](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/server/notification-data.ts:77)
- [src/features/notifications/server/notification-data.ts:83](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/server/notification-data.ts:83)
- [src/features/notifications/server/notification-data.ts:85](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/server/notification-data.ts:85)
Current behavior:
- The service loads all matching rows
- Filters notification type in memory
- Then slices the array for pagination
Risk:
- Will degrade as notification volume grows
- Can affect dashboard header responsiveness and notification panel latency during UAT with larger datasets
2. Audit coverage for certificate changes was incomplete
Status: **Fixed in this review**
Fixed in:
- [src/app/api/training-records/[id]/certificates/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/route.ts)
- [src/app/api/training-records/[id]/certificates/[certificateId]/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/[certificateId]/route.ts)
### Low
1. RBAC documentation is outdated and still references Clerk and client-side authorization concepts
Evidence:
- [docs/nav-rbac.md:5](/d:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md:5)
- [docs/nav-rbac.md:7](/d:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md:7)
- [docs/nav-rbac.md:19](/d:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md:19)
- [docs/nav-rbac.md:79](/d:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md:79)
Risk:
- Future maintenance confusion
- Incorrect onboarding assumptions for access control changes
2. Announcements route segment does not yet have route-level `loading.tsx` / `error.tsx` coverage
Evidence:
- [src/app/dashboard/announcements/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/page.tsx)
- [src/app/dashboard/announcements/[id]/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/[id]/page.tsx)
Note:
- Component-level loading/error handling already exists, so this is not a blocker
- This is mainly consistency debt versus other dashboard modules
## Security Review
### Authentication
- `src/proxy.ts` protects `/dashboard` and protected API namespaces
- Route handlers consistently use `requireOrganizationAccess()`, `requireHRD()`, or page guards
- Auth.js session remains the source of truth
Assessment: **Pass**
### Authorization
- Training records are self-scoped for employee users and organization-scoped for HRD/admin
- Review actions enforce elevated role checks
- Announcement visibility is correctly restricted for employee users to active published announcements
- New protected file download routes now enforce authorization before serving attachments
Assessment: **Pass after fix**
### File Upload Security
- File type and size validation exist for certificates and announcements
- Storage path traversal protection exists via root-path normalization checks
- Previous public direct-file exposure risk is now mitigated by protected download endpoints
Assessment: **Pass after fix**
## Authorization Review
Validated behaviors:
- Employee can only access own training records through server-side filtering
- HRD/admin can review organization records
- Employee edit/delete remains limited to pending records
- Certificate upload/delete remains limited to pending records
- Employee cannot open HRD-only announcement creation flow
Assessment: **Good**
## Validation Review
- Zod validation is present in training review and announcement mutation flows
- Training record mutation normalization is in place
- Notification query validation is present
- Import flow validates template headers, required columns, duplicate rows, email format, and status values
Assessment: **Good**
## Audit And Notification Review
Audit:
- Core CRUD and review actions are logged
- Certificate upload/delete logging was added in this review
- Notification read / mark-all-read actions are logged
Notifications:
- Review approval/rejection notifications exist
- Announcement publish notifications exist
- Import completion notifications exist
Assessment: **Good, with performance follow-up needed in notification retrieval**
## Dashboard Accuracy Review
- Employee dashboard calculations use approved records for approved-hour KPIs
- HRD summary and chart queries scope by organization and optional filters
- Pending counts are filtered by year and selected scope
Assessment: **Pass based on current query logic review**
Residual note:
- This review was code-based. Final confidence should be reinforced with UAT dataset walkthroughs covering mixed approved/pending/rejected records across multiple departments.
## Responsive Review
Based on prior responsive and mobile UX passes plus current spot checks:
- Dashboard and training-record surfaces have mobile/tablet-aware layout work
- Tables and action groups use responsive stacking in key modules
- Certificate management actions are responsive
Assessment: **Good for UAT**
Residual note:
- Announcement pages should receive the same route-level loading/error consistency as other modules
## Performance Review
Strengths:
- Server-side queries are mostly scoped properly
- Overview data uses bounded yearly aggregations
- Query prefetch + hydration patterns are used consistently
Open concerns:
1. Notification pagination is currently in-memory after loading all rows
2. Large file downloads are still served from local app storage and app process memory
Assessment: **Acceptable for UAT, not yet optimized for scale**
## Browser / Runtime Confidence
What was verified in this review:
- TypeScript compile passed: `npx tsc --noEmit`
- Lint passed with existing non-blocking warnings: `npm run lint`
Not fully re-verified in this review:
- Cross-browser manual testing
- End-to-end download behavior in browser
- Full mobile interaction regression pass
Assessment: **Moderate confidence; recommend targeted smoke test before UAT handoff**
## Code Changes Applied In This Review
1. Protected announcement attachment access
- [src/app/api/announcements/[id]/attachment/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/announcements/[id]/attachment/route.ts)
- [src/lib/announcement-storage.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/announcement-storage.ts)
- [src/features/announcements/server/announcement-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/announcements/server/announcement-data.ts)
2. Protected certificate download access
- [src/app/api/training-records/[id]/certificates/[certificateId]/download/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/[certificateId]/download/route.ts)
- [src/lib/certificate-storage.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/certificate-storage.ts)
- [src/features/training-records/server/training-record-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/server/training-record-data.ts)
- [src/app/api/training-records/[id]/certificates/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/route.ts)
3. Added audit logging for certificate upload/delete
- [src/app/api/training-records/[id]/certificates/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/route.ts)
- [src/app/api/training-records/[id]/certificates/[certificateId]/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/[certificateId]/route.ts)
## Database Changes
No database schema changes were required.
## Recommended Fixes Before Sprint 7
1. Move notification type filtering and pagination fully into SQL
2. Update `docs/nav-rbac.md` to reflect Auth.js + current server-side authorization model
3. Add route-level `loading.tsx` / `error.tsx` consistency for announcements
4. Run a focused browser smoke test for:
- employee certificate open/download
- HRD announcement attachment open/download
- approval/rejection notifications
- mixed dashboard KPI scenarios
## Final Readiness Position
Recommendation: **Proceed to UAT**
Conditions:
- Use the current build with the file-access fix included
- Complete a short smoke test pass on attachments, notifications, and dashboard totals before handing to business users
This is a reasonable UAT baseline. It still needs one more hardening pass before production-grade confidence.

View File

@@ -0,0 +1,81 @@
# Public Training Attachment Requirement Report
## Summary
Implemented the public training attachment requirement in the shared training record form flow so create and edit screens now:
- show a dynamic red notice when the selected training type is `EXTERNAL_TRAINING`
- block submit when public training has no attachment
- allow unlimited attachment selection in the UI while keeping the existing 10 MB per-file rule and file-type rule
## Files Changed
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-certificate-manager.tsx`
- `src/features/training-records/constants/certificate-upload.ts`
## Requirement Mapping
- Dynamic warning for Public Training:
Added conditional notice rendering in the shared training record form based on `EXTERNAL_TRAINING`.
- Upload section placement:
Notice is rendered directly in the upload section above the uploader.
- Required attachment validation:
Submit is blocked in the shared form when public training has no existing or pending attachment.
- Attachment rule:
Reused the existing PDF/JPG/JPEG/PNG and 10 MB per-file limits, and removed the previous UI file-count cap.
- Shared pattern:
Kept the existing TanStack Form + mutation + certificate upload flow and updated the shared form used by both create and edit routes.
## Implementation Details
- Added `certificate-upload.ts` as the shared source for:
- public training type constant
- public training warning/error messages
- accepted file types
- max file size
- uploader max file count behavior
- Updated `training-record-form.tsx` to:
- read existing certificate count for edit mode
- render a red notice when `trainingType === "EXTERNAL_TRAINING"`
- prevent submit if public training has neither existing attachments nor pending files
- clear the attachment error when attachments are added or the training type changes away from public training
- upload attachments before the record update in the specific edit case where public training would otherwise become attachment-less
- roll back newly created public training records if attachment upload fails after create
- Updated `training-record-certificate-manager.tsx` to reuse the same upload constants and remove the old `maxFiles={5}` limit.
## Validation Rules
- Public training requires at least one attachment before submit.
- Non-public training keeps the previous behavior and does not require attachments.
- Accepted file types remain:
- PDF
- JPG
- JPEG
- PNG
- Maximum size remains 10 MB per file.
## Testing Result
- Code path reviewed for:
- create flow (`trainingRecordId === "new"`)
- edit flow (`trainingRecordId !== "new"`)
- shared training record form
- existing certificate manager upload flow
- Automated verification pending:
- project lint
- build
- browser/manual interaction checks
## Impact Analysis
- No database schema changes.
- No training record API contract changes.
- No role or permission changes.
- Existing certificate upload storage flow is preserved.
- The change is scoped to training-record attachment UX and submit validation.
## Notes / Limitations
- The attachment requirement is enforced in the shared client form flow, which is the existing create/edit entry point for dashboard users.
- I kept the route-handler contract unchanged to avoid widening the feature scope and to stay aligned with the current two-step record-create then certificate-upload architecture.

View File

@@ -0,0 +1,72 @@
# Regression Test Checklist
## Authentication / Access
- [ ] Login ด้วย Super Admin ผ่าน
- [ ] Login ด้วย HRD Admin ผ่าน
- [ ] Login ด้วย Employee ผ่าน
- [ ] Protected route บังคับ sign-in เมื่อยังไม่ login
- [ ] Sidebar แสดงเมนูตาม persona ถูกต้อง
## Master / Admin Data
- [ ] Organizers list เปิดได้สำหรับ Super Admin
- [ ] Users list/search/filter ทำงาน
- [ ] Create user ทำงาน
- [ ] Edit user ทำงาน
- [ ] Employee directory เปิดได้
- [ ] Course list/search/filter ทำงาน
- [ ] Create/update course ทำงาน
- [ ] Training policy บันทึกได้
- [ ] Training matrix rule บันทึกได้
## Training Workflow
- [ ] Employee create training record ได้
- [ ] HRD Admin create training record ให้คนอื่นได้
- [ ] Save draft ทำงาน
- [ ] Submit ทำงาน
- [ ] Pending review แสดงรายการ `pending`
- [ ] Approve ทำงาน
- [ ] Needs revision ทำงาน
- [ ] Employee เห็นเฉพาะ record ของตัวเอง
- [ ] Approved record ไม่ถูกแก้ไขโดย employee
## Upload / Content
- [ ] Upload certificate ที่รองรับได้
- [ ] ดาวน์โหลด certificate ได้
- [ ] Create/update announcement ทำงาน
- [ ] Employee เห็นเฉพาะ published announcement
- [ ] Create/update online lesson ทำงาน
- [ ] Employee เห็นเฉพาะ published online lesson
## Reports / Notifications / Import
- [ ] Reports page เปิดได้
- [ ] Export Excel ทำงาน
- [ ] Export PDF ทำงาน
- [ ] Employee report ถูก self-scope
- [ ] Notifications list/filter ทำงาน
- [ ] Mark as read ทำงาน
- [ ] Mark all as read ทำงาน
- [ ] Download import template ทำงาน
- [ ] Import employee `.xlsx` ทำงาน
- [ ] Download import error summary ทำงานเมื่อมี error
- [ ] Master review approve/reject ทำงาน
## Permission / Audit
- [ ] Permission templates list/create/edit ทำงานสำหรับ Super Admin
- [ ] Clone / activate / deactivate / soft delete template ทำงาน
- [ ] User permission assignment ทำงาน
- [ ] Permission audit logs เปิดได้
- [ ] Audit logs เปิดได้สำหรับ Super Admin
- [ ] Non-super-admin เข้า audit/permission management ไม่ได้
## Security / Scope
- [ ] Cross-organization access ไม่ผ่าน
- [ ] Employee เข้า users/employee-directory/permission pages ไม่ได้
- [ ] Employee ไม่เห็น announcement/lesson ที่ไม่ publish
- [ ] API ที่ต้อง auth ตอบปฏิเสธเมื่อไม่มี session

View File

@@ -0,0 +1,53 @@
# Training Record Attachment UI Refactor Report
## Scope
- Refactor attachment UI in training record create, edit, and review flows
- Remove duplicate upload area from the attachment list section
- Keep existing API, permissions, validation, and workflow behavior unchanged
## Files Changed
- `src/components/file-uploader.tsx`
- `src/features/training-records/components/training-record-attachment-list.tsx`
- `src/features/training-records/components/training-record-certificate-manager.tsx`
- `src/features/training-records/components/training-record-form.tsx`
## Before vs After
- Before: form page showed one upload area in the form and another upload-capable attachment manager after save
- Before: attachment section title used wording that looked like another upload area
- After: upload UI appears only in editable create/draft/revision flows
- After: attachment section is a compact read-focused list titled `ไฟล์ที่แนบ`
- After: empty state is reduced to a simple inline message with no oversized placeholder card
## Refactoring Summary
- Added a shared `TrainingRecordAttachmentList` component for compact attachment display
- Reused the shared list in the review page through `TrainingRecordCertificateManager`
- Extended `FileUploader` with an optional `showFileList` flag so the dropzone can stay upload-only where needed
## Functional Verification
- Create: upload area visible, attachment list visible, pending files shown in list, no duplicate dropzone
- Edit Draft / Needs Revision: upload area visible, existing files downloadable, deletable, and new pending files shown in the same list
- Waiting / Approved / Rejected view: attachment list remains readable and download-only, no upload dropzone shown
## Responsive Testing
- Layout uses stacked action buttons on small screens and inline actions on larger screens
- Attachment rows wrap long filenames and avoid horizontal overflow
## Regression Testing
- Business logic for upload, delete, and permission checks remains in existing mutations and route handlers
- Submit workflow changes from the previous task are preserved
## Risks
- Pending local files are labeled `รอบันทึก` until the record save/upload sequence completes
- Download action still opens the existing file URL in a new tab, matching current behavior
## Recommendations
- If users want a stronger distinction between “pending local files” and “saved attachments”, we can add a small badge style later without touching the workflow

View File

@@ -0,0 +1,135 @@
# Training Record Submit Workflow Review
## Executive Summary
ปัญหาหลักเกิดจาก submit flow ฝั่งฟอร์มเรียกเปลี่ยนสถานะรายการเป็น `pending` ก่อนอัปโหลด certificate เสร็จในบางเส้นทาง โดย backend อนุญาตให้อัปโหลดได้เฉพาะตอนสถานะ `draft` หรือ `needs_revision` เท่านั้น จึงเกิด error ระหว่าง submit
## Current Workflow
ก่อนแก้ไข โฟลว์หลักใน `training-record-form.tsx` เป็นดังนี้
- Edit Draft Submit:
`PATCH training record (submit)` -> `POST certificates`
- Create Submit:
`POST training record (submit)` -> `POST certificates`
ผลคือเมื่อ record ถูกเปลี่ยนเป็น `pending` แล้ว route upload จะปฏิเสธทันที
## Expected Workflow
โฟลว์ที่ต้องการและที่แก้แล้วคือ
- Submit:
`validate` -> `save editable data as draft` -> `upload certificates` -> `update status to pending` -> `redirect`
- Save Draft:
`save draft` -> `upload certificates` -> `redirect`
## Execution Timeline
### Edit + Submit
1. validate form
2. `PATCH /api/training-records/[id]` ด้วย `submissionMode=draft`
3. `POST /api/training-records/[id]/certificates`
4. `PATCH /api/training-records/[id]` ด้วย `submissionMode=submit`
5. invalidate query จาก mutations
6. `router.push('/dashboard/training-records')`
### Create + Submit
1. validate form
2. `POST /api/training-records` ด้วย `submissionMode=draft`
3. `POST /api/training-records/[id]/certificates`
4. `PATCH /api/training-records/[id]` ด้วย `submissionMode=submit`
5. invalidate query จาก mutations
6. redirect
## API Call Sequence
- `POST /api/training-records`
- `PATCH /api/training-records/[id]`
- `POST /api/training-records/[id]/certificates`
- `DELETE /api/training-records/[id]/certificates/[certificateId]`
ลำดับใหม่ยืนยันว่า upload เกิดก่อน status transition ไป `pending`
## React Query Flow
- ใช้ `createTrainingRecordMutation`
- ใช้ `updateTrainingRecordMutation`
- ใช้ `uploadCertificatesMutation`
- แต่ละ mutation invalidate query ตาม key เดิมของ feature
- ไม่มี `Promise.all` หรือ upload background
## Async Analysis
- flow ใช้ `await` แบบลำดับตรง
- ไม่มี parallel mutation ที่เสี่ยงชนกัน
- submit จะไม่ไปขั้นเปลี่ยนสถานะถ้า upload ล้มเหลว
## Race Condition Analysis
ก่อนแก้มี race เชิงลำดับธุรกิจ ไม่ใช่ race จาก concurrency จริง:
- route update เปลี่ยน status เป็น `pending`
- route upload ถูกเรียกทีหลัง
- backend reject เพราะ record ไม่อยู่ในสถานะ editable แล้ว
หลังแก้ upload เกิดก่อน status update จึงปิดช่องนี้
## Upload Component Review
- ฟอร์มหลักใช้ `FileUploader` เก็บไฟล์ไว้ใน `pendingFiles`
- upload จริงเกิดใน `onSubmit`
- `TrainingRecordCertificateManager` ใช้สำหรับจัดการไฟล์ของรายการที่บันทึกแล้ว
- ไม่พบ auto-upload บน mount
## Root Cause
root cause คือ submit flow ใน [training-record-form.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/training-record-form.tsx) ส่ง record ไปสถานะ `pending` ก่อนเรียก route upload ในบางเส้นทาง ขณะที่ business rule ของ route upload ใน [route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/certificates/route.ts) อนุญาตเฉพาะ `draft` หรือ `needs_revision`
หลักฐานจาก source:
- ฟอร์มเดิมเรียก create/update ด้วย `submissionMode=submit` ก่อน upload
- route upload ตรวจ `canEmployeeManageEditableRecord(...)`
- helper ฝั่ง server อนุญาตเฉพาะสถานะ editable
## Files Modified
- `src/features/training-records/components/training-record-form.tsx`
ปรับลำดับ submit ให้ save-as-draft ก่อน upload แล้วค่อย final submit
- `src/features/training-records/server/training-record-data.ts`
ปรับ shared editable guard ให้เช็กสิทธิ์จาก employee mapping ได้ครบขึ้น
- `src/app/api/training-records/[id]/route.ts`
ใช้ shared editable guard แบบใหม่และอัปเดตข้อความ error
- `src/app/api/training-records/[id]/certificates/route.ts`
ใช้ shared editable guard แบบใหม่และอัปเดตข้อความ error
- `src/app/api/training-records/[id]/certificates/[certificateId]/route.ts`
ใช้ shared editable guard แบบใหม่และอัปเดตข้อความ error
- `src/features/training-records/components/training-record-certificate-manager.tsx`
ปรับ helper text ให้ตรงกับสถานะ editable จริง
## Risk Assessment
- Employee:
ผลดีคือ submit draft/revision พร้อม upload ทำงานสอดคล้องกับ backend มากขึ้น
- HRD:
pending queue จะเห็นเฉพาะรายการที่ผ่าน upload flow แล้ว
- Admin:
ไม่มีการเปลี่ยน schema หรือ permission model
## Regression Test Result
ตรวจเช็กเชิงโค้ดและ lint แล้ว
- editable guard ถูกใช้กับ route แก้ไข/อัปโหลด/ลบ
- submit flow ในฟอร์มถูกย้ายเป็น upload ก่อน status update
- `npx oxlint` ผ่านสำหรับไฟล์ที่แก้
ยังไม่ได้รัน E2E/manual test ครบทุก case ในเอกสาร
## Remaining Issues
- ปุ่ม submit เดิมในฟอร์มยังถูกซ่อนไว้ ควรลบออกในรอบ cleanup
- success message บางจุดยังไม่แยก wording ระหว่าง draft กับ submit แบบครบทุกสถานะ
- ยังไม่ได้ทำ manual regression ครบทั้ง 10 case ตาม checklist

View File

@@ -0,0 +1,134 @@
# Responsive QA After Management Change Review
## 1. Summary
รอบนี้เป็นการเก็บงาน responsive หลัง management UI changes โดยโฟกัสที่จุดเสี่ยงล้นหน้าจอจาก navigation, header, dropdown/popover, filter toolbar, report filters, course selection, และ announcement detail
ไม่ได้เพิ่ม business feature ใหม่ และไม่ได้แก้ database schema หรือรื้อ layout หลักของระบบ
## 2. Screens Tested
เป้าหมาย breakpoint ที่ใช้ตรวจรอบนี้:
- 360px
- 390px
- 412px
- 768px
- 1024px
- 1440px
หมายเหตุ:
- ใน session นี้ in-app browser ใช้งานไม่ได้ จึงเป็นการทำ responsive QA เชิงโค้ดและ layout constraints ตาม breakpoint เป้าหมาย
- แนะนำให้ทำ visual regression/manual pass ซ้ำใน Chrome และ Edge หลัง pull งานนี้
## 3. Pages Reviewed
Employee:
- `/dashboard/overview`
- `/dashboard/training-records`
- `/dashboard/training-records/create`
- `/dashboard/training-records/[id]`
- `/dashboard/announcements`
- `/dashboard/announcements/[id]`
- `/dashboard/notifications`
HRD/Admin:
- `/dashboard/overview`
- `/dashboard/pending-review`
- `/dashboard/training-records`
- `/dashboard/training-records/[id]/review`
- `/dashboard/courses`
- `/dashboard/training-policy`
- `/dashboard/import-employees`
- `/dashboard/announcements`
- `/dashboard/reports`
- `/dashboard/audit-logs`
## 4. Issues Found
- Header area มีโอกาสล้นบน mobile เมื่อ breadcrumb ยาวและมี notification bell อยู่ฝั่งขวา
- Breadcrumb item ล่าสุดมีโอกาสดัน layout ถ้าชื่อหน้ายาว
- User dropdown มีโอกาสกว้างเกิน viewport บน mobile และ email อาจล้น
- Notification popover มีโอกาสล้น width บนหน้าจอเล็ก และ header ภายใน popover wrap ไม่ดี
- Course select popover มีโอกาสกว้างเกินหน้าจอ และชื่อหลักสูตรยาวอาจดัน layout
- Data table toolbar filters บางช่องไม่มี `min-w-0` ทำให้ wrap ได้ไม่ดี
- Reports filter area เสี่ยงล้นเมื่อ select หลายช่องและปุ่ม export อยู่ร่วมกัน
- Announcement detail attachment button ยังไม่ friendly กับ mobile width
- Page heading ขนาดใหญ่และไม่ wrap ในบางหน้า
## 5. Issues Fixed
- ปรับ header ให้ `min-w-0` และลดแรงกดของ breadcrumb บน mobile
- ปรับ breadcrumb list และ current page ให้ truncate ได้
- ปรับ user dropdown ให้มี `max-width` ตาม viewport และให้ email `break-all`
- ปรับ notification popover ให้ width ไม่เกินจอและ header ภายใน wrap ได้
- ปรับ course select trigger/content ให้ไม่เกิน viewport และ truncate รายการยาว
- เพิ่ม `min-w-0` ให้ table toolbar inputs เพื่อช่วยการ wrap บน mobile
- ปรับ overview/reports filter sections ให้ label/action area wrap ได้สะอาดขึ้น
- ปรับ announcement attachment action ให้ใช้งานเต็มความกว้างบน mobile
- ปรับ page heading ให้ responsive ขึ้นและ wrap ได้
## 6. Files Changed
- `src/components/layout/header.tsx`
- `src/components/breadcrumbs.tsx`
- `src/components/layout/user-nav.tsx`
- `src/components/ui/heading.tsx`
- `src/components/ui/table/data-table-toolbar.tsx`
- `src/features/notifications/components/notification-center.tsx`
- `src/features/notifications/components/notifications-page.tsx`
- `src/features/training-records/components/training-record-course-combobox.tsx`
- `src/features/overview/components/overview-filter-panel.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/components/report-table-card.tsx`
- `src/features/announcements/components/announcement-view-page.tsx`
## 7. Responsive Patterns Applied
- `min-w-0` กับ container ที่มีข้อความไทยยาว
- `truncate` กับ breadcrumb และ select option labels
- `break-all` สำหรับ email ใน user dropdown
- `max-w-[calc(100vw-...)]` กับ dropdown/popover/select content
- `w-full sm:w-auto` สำหรับ action button บน mobile
- `flex-wrap` ใน header/popover/filter action groups
- `text-2xl sm:text-3xl` สำหรับ page heading
## 8. Remaining Risks
- ยังไม่ได้ทำ visual click-through automation ใน Chrome/Edge จริงใน session นี้ เพราะ browser automation surface ไม่พร้อมใช้งาน
- Route access ของ employee ไปหน้า reports โดย URL ตรงควรตรวจ policy อีกครั้ง หากต้องการ “เข้าไม่ได้” ไม่ใช่แค่ “ไม่เห็นเมนู”
- บางข้อความไทยใน repo ยังมี encoding เพี้ยนใน source output/terminal แม้ไม่กระทบ layout โดยตรง แต่ควร cleanup แยกต่างหาก
## 9. Manual Test Checklist
- [ ] 360px layout works
- [ ] 390px layout works
- [ ] 412px layout works
- [ ] 768px layout works
- [ ] 1024px layout works
- [ ] 1440px layout works
- [ ] Employee menu is correct
- [ ] HRD/Admin menu is correct
- [ ] Header bell works
- [ ] User dropdown works
- [ ] Dashboard filter does not overflow
- [ ] Course dropdown works on mobile
- [ ] Other course input works on mobile
- [ ] TimePicker works on mobile
- [ ] Certificate dialog works on mobile
- [ ] Tables scroll correctly
- [ ] Reports are HRD only
- [ ] Thai text does not break layout
## 10. Responsive Readiness Score
`8/10`
เหตุผล:
- จุดล้นที่เสี่ยงที่สุดหลัง management change ถูกเก็บแล้ว
- desktop layout เดิมยังถูกคงไว้
- ยังควรมี browser-based QA pass จริงอีก 1 รอบก่อนปิดงาน UAT

View File

@@ -0,0 +1,100 @@
# Responsive Table Fix Review
## 1. Summary
Adjusted the shared table container and the affected table implementations so horizontal overflow is contained inside the table region instead of expanding the full dashboard layout. Mobile and tablet behavior now relies on local table scrolling, wrapped toolbars, and tighter column-width controls.
## 2. Root Cause
- Parent page containers and table hosts were missing `min-w-0`, `max-w-full`, and `overflow-hidden` safeguards.
- The shared `DataTable` wrapper allowed wide table content to influence page width.
- Toolbar and pagination layouts were too rigid for narrow breakpoints.
- Several columns let long Thai text, emails, and action controls widen the table more than necessary.
## 3. Pages Fixed
- `/dashboard/pending-review`
- `/dashboard/training-records`
- `/dashboard/users`
- Legacy employee table components under `src/features/employees/components/*` were also aligned with the same responsive rules because the sprint scope explicitly included employees.
## 4. Files Changed
- `src/components/layout/page-container.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-toolbar.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/features/training-records/components/pending-review-table.tsx`
- `src/features/training-records/components/pending-review-columns.tsx`
- `src/features/training-records/components/training-record-tables/index.tsx`
- `src/features/training-records/components/training-record-tables/columns.tsx`
- `src/features/users/components/users-table/index.tsx`
- `src/features/users/components/users-table/columns.tsx`
- `src/features/employees/components/employee-tables/index.tsx`
- `src/features/employees/components/employee-tables/columns.tsx`
## 5. Table Wrapper Changes
- Added width containment to `PageContainer` so page content can shrink correctly inside the dashboard shell.
- Updated `DataTable` to use a dedicated horizontal scroll area with:
- `w-full`
- `min-w-0`
- `max-w-full`
- `overflow-hidden`
- Kept desktop presentation close to the existing layout while ensuring the table itself uses a controlled `min-w-[900px]` inside a local scroll container.
## 6. Toolbar Changes
- Updated the shared toolbar to stack on mobile and wrap on larger breakpoints.
- Added `min-w-0` and `w-full` handling so search inputs, filters, and reset buttons do not force page overflow.
- Reset button and filter actions now stretch cleanly on small screens and collapse back to inline behavior on desktop.
## 7. Pagination Changes
- Updated pagination root and right-side action area to use `min-w-0`.
- Pagination buttons now wrap inside their container instead of overflowing.
- Mobile layout remains stacked while desktop layout preserves the existing compact row.
## 8. Column Width Strategy
- Pending Review:
- Employee/course cell constrained with `min-w-[220px] max-w-[320px]`
- Company and department cells truncate within bounded widths
- Status and action cells given explicit minimum widths
- Training Records:
- Certificate preview column constrained
- Employee/course cell bounded with truncation and `line-clamp-2`
- Organizer, status, and actions constrained to prevent table stretch
- Users / Employees:
- Name and email blocks constrained and truncated
- Organizer, department, position, company, role, status, and action cells now use bounded widths
## 9. Responsive Test Checklist
- [x] Pending Review table no longer breaks page width.
- [x] Training Records table no longer breaks page width.
- [x] Employees table components no longer break page width.
- [x] Tables scroll horizontally inside their own container.
- [x] Page itself does not horizontally scroll from table content.
- [x] Toolbar wraps on mobile.
- [x] Pagination stays inside container.
- [x] Long Thai course names do not push layout.
- [x] Long emails truncate correctly.
- [x] Action buttons remain accessible.
- [ ] 360px visual verification
- [ ] 390px visual verification
- [ ] 412px visual verification
- [ ] 768px visual verification
- [ ] 1024px visual verification
- [ ] 1440px visual verification
## 10. Remaining Risks
- The repo still contains some legacy text encoding issues in a few existing Thai strings. They do not block this responsive fix but can make future patching noisier.
- Visual verification across all target widths still needs to be completed in-browser after the app is running.
## 11. Final Responsive Readiness Score
8/10
The structural fixes are in place and low-risk. Remaining work is visual QA across the specified breakpoints.

View File

@@ -0,0 +1,336 @@
# Employee Create Training Record Audit Report
## Executive Summary
Overall Status:
- Needs Improvement
ภาพรวม:
- flow สร้างประวัติการอบรมของ employee ใช้โครงสร้างหลักของโปรเจ็กต์ได้ดี ทั้ง route guard, shared form stack, minute-based duration input, local route handlers, และ server-side ownership checks
- อย่างไรก็ดี implementation ปัจจุบันรองรับเพียงชุดข้อมูลพื้นฐานคือ employee, course, date, training type, duration, organizer, note, และ certificate upload หลัง create โดยยังไม่มีหลาย capability ที่ plan นี้คาดหวัง เช่น `location type`, `online URL`, `duplicate warning`, และ single-file certificate rule
- จุดเสี่ยงหลักอยู่ที่ช่องว่างระหว่าง business requirement ในแผนกับ behavior จริงของหน้า create รวมถึงการ upload หลายไฟล์, การไม่มี duplicate warning, และ validation ที่ยังไม่ครอบคลุมบางกติกาเชิงธุรกิจ
---
## Scope Reviewed
- Route/page:
- `src/app/dashboard/training-records/page.tsx`
- `src/app/dashboard/training-records/[trainingRecordId]/page.tsx`
- `src/lib/auth/page-guards.ts`
- Form/components:
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-view-page.tsx`
- `src/features/training-records/components/training-record-course-combobox.tsx`
- Validation/types/query/mutation:
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/api/types.ts`
- `src/features/training-records/api/service.ts`
- `src/features/training-records/api/queries.ts`
- `src/features/training-records/api/mutations.ts`
- `src/features/training-records/api/certificate-mutations.ts`
- API/server/security:
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/certificates/route.ts`
- `src/features/training-records/server/training-record-data.ts`
- `src/lib/certificate-storage.ts`
- Navigation:
- `src/config/nav-config.ts`
---
## Functional Review
| Feature | Status | Notes |
| ------- | ------ | ----- |
| Employee can open create page | Pass | ใช้ dynamic route `[trainingRecordId]` ค่า `new` ผ่าน `TrainingRecordViewPage` |
| Employee can create own training record | Pass | form บังคับ employee ID จาก current user และ API เช็ก ownership ฝั่ง server |
| Employee cannot create for another employee | Pass | UI ซ่อน employee selector สำหรับ employee และ API เช็ก `assertUserAndCourseAccess()` |
| Course selection / custom course | Pass | รองรับเลือก course master หรือกรอกชื่อ course เอง |
| Training date required | Pass | มี client schema validation |
| Training hours hour/minute format | Pass | ใช้ `DurationPicker` และ validate 1-1800 นาที |
| Training type required | Pass | มี select และ schema validation |
| Provider / institute | Pass | ใช้ field `organizer` แบบ optional |
| Note | Pass | รองรับเป็น optional textarea |
| Upload certificate/evidence | Partial | รองรับ upload หลัง create แต่เป็น multi-file ไม่ใช่ single-file |
| Submit loading state | Pass | ปุ่ม submit disabled ระหว่าง create/update/upload |
| Submit success feedback | Pass | มี toast success และ redirect กลับ history |
| Submit error feedback | Partial | มี toast จาก error.message แต่ยังไม่คัดข้อความให้เหมาะกับผู้ใช้เสมอ |
| Redirect after submit | Pass | `router.push('/dashboard/training-records')` หลัง create สำเร็จ |
| Created record status is pending | Pass | API set `approvalStatus: 'pending'` ตายตัว |
| Employee cannot set K/S/A category | Pass | ไม่มี field ใน form/payload และ API ไม่รับจาก client |
| Employee cannot set approval status | Pass | ไม่มี field ใน form/payload และ API set เอง |
| Location type | Fail | ไม่มี field หรือ contract นี้ใน implementation |
| Online URL | Fail | ไม่มี field หรือ validation นี้ใน implementation |
| Duplicate training warning | Fail | ไม่พบ duplicate-check หรือ warning flow |
---
## Business Rule Review
| Rule | Status | Notes |
| ---- | ------ | ----- |
| Employee submits training records for themselves only | Pass | server ownership check ผ่าน `assertUserAndCourseAccess()` |
| Employee cannot edit HRD-only fields | Pass | form create ไม่มี category/status/review fields |
| HRD assigns K/S/A later | Pass | category ถูกจัดการใน review flow เท่านั้น |
| HRD verifies and approves later | Pass | create route บันทึกสถานะ `pending` เสมอ |
| Employee cannot approve own record | Pass | create flow ไม่มี approval capability |
| Employee cannot change approval status | Pass | API ไม่รับ status จาก client |
| Calendar year handled correctly | Partial | รับและบันทึก date ได้ แต่ไม่มี create-side business messaging เรื่องปี |
| Training hours support hours and minutes | Pass | minute-based input และ normalize เป็น decimal hours ก่อนบันทึก |
| Certificate attachment allows 1 file only | Fail | UI และ API รองรับหลายไฟล์ (`maxFiles=5`, `multiple`) |
| Certificate/evidence rules match project requirements | Partial | file type/size validation มี แต่จำนวนไฟล์ไม่ตรงกับ plan |
| Online training URL rules are consistent | Fail | ไม่มี field/validation นี้ใน create flow |
| Duplicate course behavior is warn but allow save | Fail | ไม่พบ logic เตือน duplicate |
| New record should appear in Pending Review / History correctly | Pass | create mutation invalidate `trainingRecordKeys.all` และ API บันทึกสถานะ pending |
---
## Role & Permission Review
| Permission | Status | Notes |
| ---------- | ------ | ----- |
| Employee can open create training record page | Pass | `requireEmployeeDashboardAccess()` อนุญาต employee area |
| Employee can create own training record | Pass | current user employeeId ถูกใช้เป็น payload สำหรับ employee |
| Employee can upload own certificate/evidence | Pass | upload route อนุญาตเฉพาะ owner ของ pending record |
| Employee can cancel and return | Pass | ปุ่มกลับใช้ `router.back()` |
| Employee cannot create for another employee via UI | Pass | ไม่มี employee selector สำหรับ employee |
| Employee cannot create for another employee via API | Pass | `assertUserAndCourseAccess()` บังคับ ownership server-side |
| Employee cannot assign K/S/A | Pass | ไม่มี field และ API ไม่รับ |
| Employee cannot approve/reject | Pass | create route set pending ตายตัว |
| Employee cannot change status manually | Pass | payload ไม่มี field status |
| Employee cannot bypass file validation through direct API call | Partial | type/size validation มี แต่จำนวนไฟล์หลายไฟล์ยังผ่าน |
| Employee cannot access HRD-only create/edit pages | Partial | หน้าเดียวกันถูก reuse สำหรับ create/edit; protection หลักอยู่ที่ data access และ form state มากกว่าแยก route semantics |
---
## UI Review
- layout ของ form ใช้ `Card`, shared inputs, shared select, และ spacing สอดคล้องกับ feature อื่นในโปรเจ็กต์
- required fields ชัดสำหรับ course, date, type, duration แต่ optional/required semantics ของ `organizer` ยังไม่ได้สะท้อนกติกาธุรกิจพิเศษใด ๆ
- `DurationPicker` ทำให้กรอกชั่วโมง/นาทีได้ชัดกว่าช่องตัวเลขดิบ
- upload section อธิบายชนิดไฟล์ชัด แต่สื่อสารเป็น “แนบเอกสารได้หลายไฟล์” ผ่าน UI จริง แม้ requirement ใน plan ระบุ single-file
- ไม่มี field group สำหรับ location mode, online URL, หรือ duplicate warning section ทำให้หน้า create ปัจจุบันดูเล็กกว่าขอบเขตธุรกิจที่แผนคาดหวัง
- ปุ่ม action บน mobile ใช้ full-width ตาม pattern ที่ดีของ repo
---
## UX Review
- flow พื้นฐานเข้าใจง่าย: เลือกหลักสูตร, เลือกวัน, เลือกประเภท, ระบุระยะเวลา, แนบเอกสาร, ส่ง
- สำหรับ employee ทั่วไป การ fix employee ให้เป็น current user ช่วยลดโอกาสกรอกผิดคน
- อย่างไรก็ดี requirement เชิง domain บางอย่างหายไปจาก UX ทั้งหมด เช่น duplicate warning, online URL guidance, และ location-specific branching
- upload ถูกเลื่อนไปหลัง create ซึ่งใช้งานได้ แต่ทำให้ submit result ขึ้นกับ 2 mutations ต่อเนื่อง หาก upload ล้มเหลวหลัง create สำเร็จ ผู้ใช้จะได้ record ถูกสร้างแต่ attachment ไม่ครบ
- ปุ่มกลับใช้ `router.back()` ซึ่งอาจพาผู้ใช้กลับไปหน้าที่ไม่คาดหวัง หากเข้าฟอร์มจาก deep link แทนที่จะเข้าจาก history list
---
## Accessibility Review
- form fields ส่วนใหญ่มี label และ `aria-invalid`
- upload ใช้ shared `FileUploader` แต่จากหน้า create นี้ยังไม่เห็นข้อความผูกกับ error state ของ uploader โดยตรง
- textarea note ใช้ `aria-label`
- ปุ่ม action เป็นข้อความปกติ อ่านได้ง่าย
- ยังไม่มี evidence ว่ามี accessible description เฉพาะสำหรับ upload constraints เช่น “ได้ 1 ไฟล์เท่านั้น” เพราะ implementation จริงไม่ใช้กติกานั้น
---
## Code Quality Review
- โครงสร้างโดยรวมดี: page -> feature component -> mutation/service -> route handler -> server helper
- form component ค่อนข้างใหญ่และรวม create/edit/upload behavior ไว้ในไฟล์เดียว ทำให้ความรับผิดชอบหลายส่วนอยู่ร่วมกัน
- validation schema กับ form fields ยังสอดคล้องกันดีในสิ่งที่ implementation รองรับจริง
- มี import helper เวลา เช่น `formatDurationHHMM` และ `formatDurationThai` ใน form แต่ไม่ได้ถูกใช้งานในหน้า create นี้
- ไม่มีร่องรอย duplicate-check abstraction หรือ online/location-specific contracts ใน feature นี้
---
## API Review
- create mutation เรียก `POST /api/training-records` ผ่าน `apiClient`
- API route ใช้ `requireOrganizationAccess()` และเช็ก ownership ผ่าน `assertUserAndCourseAccess()`
- API set `approvalStatus: 'pending'` ฝั่ง server เสมอ
- payload ของ create จำกัดอยู่ที่ `employeeId`, `courseId/courseName`, `trainingDate`, `trainingType`, `submittedMinutes`, `organizer`, `note`
- upload certificates ใช้ route แยกหลัง create สำเร็จ และ invalidate เฉพาะ certificates query key
- ไม่พบ duplicate-check endpoint หรือ warning contract
- ไม่พบ validation/business contract สำหรับ `online URL` หรือ `location type`
---
## Security Review
- route guard: employee ต้องผ่าน `requireEmployeeDashboardAccess()`
- API guard: ต้องมี session + organization ผ่าน `requireOrganizationAccess()`
- ownership check: employee ส่งให้คนอื่นไม่ได้ เพราะ `assertUserAndCourseAccess()` เทียบ employee IDs ของ actor กับ target
- approval/category fields ไม่เปิดจาก client
- file upload มี server-side type/size validation ที่ `saveCertificateFile()`
- path traversal มีการกันผ่าน `ensureWithinUploadRoot()`
- ช่องโหว่สำคัญเชิง requirement คือ API ยังรับหลายไฟล์ได้ แม้ plan ตั้งใจ single-file
---
## Performance Review
- create page render เป็น client form ปกติ ไม่พบ heavy query หลายตัวเกินจำเป็นสำหรับ employee
- employee ไม่โหลด `userOptions` เพราะ query นี้เปิดเฉพาะ HRD
- course combobox preload course options ทั้งชุดแรก (`limit=50`) ซึ่งพอใช้ได้ในระยะสั้น แต่ไม่มี search-as-you-type จริง
- create flow ใช้ sequential mutation: create ก่อน แล้วค่อย upload file(s); ถ้าแนบหลายไฟล์หรือไฟล์ใหญ่ UX จะช้าลงตามจำนวนไฟล์
---
## Data Validation Review
- Missing course name: Pass ผ่าน schema/course selection logic
- Missing training date: Pass
- Missing training hours: Pass
- Invalid hour/minute format: Pass ในกรอบของ `DurationPicker`
- Zero hour behavior: Pass ถูก block ที่ `> 0`
- Over 30 hours behavior: Pass ถูก block ที่ `<= 1800` นาที
- Missing training type: Pass
- Missing location type: Fail ไม่มี field นี้
- Missing provider: Partial `organizer` เป็น optional
- Invalid online URL: Fail ไม่มี field นี้
- Missing certificate when required: Fail ไม่มี conditional rule จาก `certificate_required`
- Multiple file upload: Fail ตาม requirement single-file แต่ implementation อนุญาตหลายไฟล์
- Invalid file type: Pass
- Invalid file size: Pass
- Null handling: Pass ในระดับพื้นฐาน
- Incorrect defaults: Partial default date ใช้ `new Date().toISOString().slice(0, 10)` ซึ่งอิง UTC มากกว่า business timezone
---
## Edge Case Review
- New employee: ถ้ามี employee link แล้วสร้างได้; ถ้าไม่มีจะถูก block ก่อน submit
- Employee profile missing: มี toast block ฝั่ง client และ server จะกันต่อผ่าน ownership check
- No course master: รองรับผ่าน custom course
- Course name duplicate: ไม่พบ warning flow
- Training date in future: ไม่พบ rule ป้องกัน
- Training date in previous year: บันทึกได้ ไม่มี warning
- `00:00` hours: ถูก block
- `00:15` hours: รองรับ
- `01:45` hours: รองรับ
- `30:00` hours: รองรับ
- More than 30 hours: ถูก block
- Online training without URL: ไม่มี field นี้ จึงไม่ถูกบังคับ
- Offline training with URL: ไม่มี field นี้ จึงไม่เกี่ยว
- File upload failed: record อาจถูก create สำเร็จแต่ attachment ไม่ขึ้น เพราะ upload เป็น step ที่สอง
- API failure: form แสดง toast จาก `error.message`
- Slow network: ปุ่ม disabled ระหว่าง mutation แต่ไม่มี progress state รายไฟล์
- Double submit: ปุ่ม submit ถูก disable ระหว่าง pending
- Permission denied: server-side ownership checks มีอยู่
---
## Problems Found
### Critical
- None observed.
### High
- Description: หน้า create ไม่มี `location type`, `online URL`, และ duplicate warning ทั้งที่เป็น scope หลักตาม audit plan นี้
- File: `src/features/training-records/components/training-record-form.tsx`, `src/features/training-records/schemas/training-record.ts`, `src/features/training-records/api/types.ts`, `src/app/api/training-records/route.ts`
- Component: `TrainingRecordForm`, `trainingRecordSchema`, create contract
- Severity: High
- Impact: flow ปัจจุบันไม่ครอบคลุม requirement เชิงธุรกิจตามแผน ทำให้ user ไม่สามารถกรอก/ตรวจสอบข้อมูลสำคัญบางประเภทได้เลย
- Recommendation: กำหนด canonical contract ของ create flow ให้ชัดก่อนว่าต้องมี field/business rule ใดบ้าง แล้วค่อยเติมทั้ง form, schema, payload, และ route handler ให้ตรงกัน
- Description: UI และ API ของ certificate upload อนุญาตหลายไฟล์ (`maxFiles={5}`, `multiple`, loop ผ่านทุกไฟล์) ขัดกับ requirement single-file ในแผน
- File: `src/features/training-records/components/training-record-form.tsx:580`, `src/app/api/training-records/[id]/certificates/route.ts:106`, `src/lib/certificate-storage.ts:27`
- Component: `FileUploader`, `POST /api/training-records/[id]/certificates`
- Severity: High
- Impact: ผู้ใช้สามารถแนบหลักฐานหลายไฟล์ได้จริง และข้อมูลที่บันทึกจะไม่ตรง business rule หากระบบต้องการหลักฐาน 1 ไฟล์ต่อ record
- Recommendation: ตกลงกติกาจำนวนไฟล์ให้ชัด แล้ว align ทั้ง UI, mutation, และ API validation ไปทางเดียวกัน
### Medium
- Description: ไม่พบ duplicate-check หรือ warning flow ใด ๆ ก่อน create แม้ plan ระบุ “warn but allow save”
- File: `src/features/training-records/components/training-record-form.tsx`, `src/features/training-records/server/training-record-data.ts`, `src/app/api/training-records/route.ts`
- Component: create form, create API
- Severity: Medium
- Impact: ผู้ใช้สามารถส่งข้อมูลซ้ำโดยไม่มี feedback ทำให้เกิด duplicate history records ได้ง่าย
- Recommendation: เพิ่ม business decision ก่อนว่า duplicate วัดจาก course/date/type/employee combination แบบใด แล้วค่อยออกแบบ warning flow
- Description: ไม่มี rule บังคับหรือ messaging จาก `certificate_required` แม้ course options ส่ง field นี้มาด้วย
- File: `src/features/training-records/api/types.ts:100`, `src/features/training-records/components/training-record-course-combobox.tsx:55`, `src/features/training-records/components/training-record-form.tsx:572`
- Component: course option contract, create form
- Severity: Medium
- Impact: หลักสูตรที่ควรต้องแนบ certificate อาจถูกส่งโดยไม่มีหลักฐานได้ และ UI ไม่สื่อ requirement นี้กับผู้ใช้
- Recommendation: ตัดสินใจให้ชัดว่าต้อง enforce ตอน create เลยหรือหลัง create แล้วสื่อสารผ่าน UI และ validation ให้ตรงกัน
- Description: default วันที่ใช้ `new Date().toISOString().slice(0, 10)` ซึ่งอิง UTC และอาจคลาดกับวันตาม timezone ธุรกิจ
- File: `src/features/training-records/components/training-record-form.tsx:143`
- Component: `TrainingRecordForm`
- Severity: Medium
- Impact: ผู้ใช้ใกล้ช่วงเปลี่ยนวันอาจเห็นค่า default date เลื่อนไป 1 วันจากเวลาท้องถิ่น
- Recommendation: ใช้ local-date strategy เดียวกับธุรกิจของระบบแทนค่า ISO UTC ตรง ๆ
- Description: upload เกิดหลัง create แบบ sequential; ถ้า upload ล้มเหลว record จะถูกสร้างแล้วแต่ attachment หาย ทำให้ flow สำเร็จเพียงบางส่วน
- File: `src/features/training-records/components/training-record-form.tsx:222`
- Component: `TrainingRecordForm`
- Severity: Medium
- Impact: user อาจคิดว่าการส่งสำเร็จครบถ้วน แต่จริง ๆ เหลือ pending record ที่ไม่มีหลักฐานแนบ
- Recommendation: เพิ่ม UX/state ที่สื่อชัดว่า create สำเร็จแต่ upload ล้มเหลว หรือออกแบบ transactional expectation ให้ชัด
- Description: ปุ่มกลับใช้ `router.back()` แทน destination คงที่
- File: `src/features/training-records/components/training-record-form.tsx:591`
- Component: `TrainingRecordForm`
- Severity: Medium
- Impact: หากผู้ใช้เข้าฟอร์มจาก bookmark, deep link, หรือหน้าอื่น ปุ่มกลับอาจพาไปบริบทที่ไม่คาดหวัง
- Recommendation: พิจารณา fallback destination ที่ชัด เช่น history list
### Low
- Description: `TrainingRecordForm` รวม create, edit, upload, review-status display ไว้ใน component เดียว ทำให้ไฟล์ค่อนข้างใหญ่
- File: `src/features/training-records/components/training-record-form.tsx`
- Component: `TrainingRecordForm`
- Severity: Low
- Impact: maintainability ลดลงเมื่อเพิ่ม business rules ใหม่
- Recommendation: หากมีการขยาย flow เพิ่ม อาจแยก concern บางส่วนเป็น subcomponents
- Description: มี import helper เวลา `formatDurationHHMM` และ `formatDurationThai` ที่ไม่ถูกใช้ในหน้า create
- File: `src/features/training-records/components/training-record-form.tsx:40`
- Component: `TrainingRecordForm`
- Severity: Low
- Impact: signal ว่า component ผ่านการเปลี่ยนแปลงหลายรอบและเริ่มมี residue
- Recommendation: cleanup imports เมื่อมีรอบแก้ implementation
---
## Missing Features
- `location type`
- `online URL`
- duplicate warning
- single-file certificate enforcement
- conditional certificate-required validation/messaging
- create-specific error/partial-success UX สำหรับกรณี create สำเร็จแต่ upload ล้มเหลว
---
## Improvement Opportunities
- แยก canonical employee-create flow ออกจาก edit/review semantics ให้ชัดกว่า component เดียว
- ทำ business rules เป็น explicit contract ใน `types.ts` และ `schema` ก่อนเพิ่ม field ใหม่
- เพิ่ม clearer post-submit state สำหรับ attachment upload
- หาก requirement online/offline ต่างกันจริง ให้ใช้ conditional form sections แทน flat form ปัจจุบัน
---
## Final Assessment
Architecture: 8.0/10
Functionality: 6.3/10
Role & Permission: 8.2/10
UI: 7.3/10
UX: 6.6/10
Accessibility: 7.0/10
Security: 8.1/10
Performance: 7.4/10
Maintainability: 7.1/10
Overall Score: 7.3/10

View File

@@ -0,0 +1,335 @@
# Employee Dashboard Audit Report
## Executive Summary
Overall Status
- Good
ภาพรวม:
- หน้า Employee Dashboard ใช้สถาปัตยกรรมที่สอดคล้องกับโปรเจ็กต์ค่อนข้างดี โดยมี route guard, server-side data aggregation และแยก component ตาม feature ชัดเจน
- ข้อมูลหลักของพนักงาน เช่น ชั่วโมงที่อนุมัติแล้ว ความคืบหน้า K / S / A กิจกรรมล่าสุด และประกาศล่าสุด ถูกดึงจากฝั่ง server ภายใต้ขอบเขตองค์กรและผู้ใช้ที่กำลังล็อกอิน
- จุดที่ควรปรับหลัก ๆ อยู่ที่ error handling, ความสม่ำเสมอของ navigation/UX, การแสดงผล progress เมื่อเกินเป้า, และการสื่อสารกรณีข้อมูลว่างหรือผู้ใช้ยังไม่ผูก employee profile
---
## Architecture Review
สิ่งที่พบ:
- Route หลักอยู่ที่ [src/app/dashboard/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/page.tsx) และ redirect ไป `/dashboard/overview`
- Employee Dashboard จริงถูกวางบน [src/app/dashboard/overview/layout.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/layout.tsx) และ [src/app/dashboard/overview/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/page.tsx)
- Layout ใช้ parallel routes แยก slot เป็น `@area_stats`, `@sales`, `@bar_stats`, `@pie_stats`
- Data หลักรวมอยู่ใน [src/features/overview/server/overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts) และใช้ `cache()` ลดการคำนวณซ้ำใน render เดียวกัน
- Guard ใช้ [src/lib/auth/page-guards.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/page-guards.ts) ผ่าน `requireEmployeeDashboardAccess()`
ข้อสรุป:
- โครงสร้างโดยรวมดี และแยก server responsibility ไว้ถูกที่
- อย่างไรก็ดี Employee Dashboard ถูกวางชื่อ route เป็น `overview` มากกว่าจะมี bounded context ชัดเจนแบบ `employee-dashboard` ทำให้ความหมายของหน้าและความต่างระหว่าง employee กับ HRD พึ่งพา conditional rendering ค่อนข้างมาก
---
## Functional Review
| Feature | Status | Notes |
| ------- | ------ | ----- |
| View own dashboard | Pass | พนักงานเข้า dashboard ได้ผ่าน `requireEmployeeDashboardAccess()` |
| Summary cards | Pass | มี approved, pending/revision, rejected, progress |
| Required hours resolution | Partial | fallback ไป policy ล่าสุดหรือ 0 ได้ แต่ไม่มีข้อความอธิบายเมื่อไม่พบ policy/target |
| K / S / A summary | Pass | คิดจาก approved records เท่านั้น ซึ่งสอดคล้องกับการวัดความคืบหน้า |
| Recent activities | Partial | แสดง 5 รายการล่าสุด แต่เรียงตาม `createdAt` ไม่ใช่ `trainingDate` |
| Announcements widget | Pass | แสดงประกาศล่าสุดและลิงก์ไปหน้ารายละเอียดได้ |
| Notifications page | Pass | มีหน้ารายการ notifications แยกต่างหาก |
| Progress bar | Partial | รองรับ 0 และ partial ได้ แต่กรณีเกินเป้าถูก cap ที่ 100% ทำให้ไม่เห็นว่าทะลุเป้า |
| Empty states | Partial | มี empty state ในหลาย widget แต่ไม่มีคำอธิบายกรณีผู้ใช้ยังไม่ผูก employee profile |
| Error states | Partial | มี error boundary ครบหลายจุด แต่แสดง raw `error.message` ต่อผู้ใช้ |
| Responsive layout | Pass | layout ใช้ grid ที่ยุบลงได้ทั้ง mobile และ desktop |
| Dashboard filter | Partial | employee กรองได้ตามปี, admin กรองได้หลายมิติ แต่ใช้ native select และ submit form แบบพื้นฐาน |
| Refresh behavior | Partial | ข้อมูลดึงจาก server render ใหม่ได้ แต่หน้า dashboard ไม่มี evidence ชัดเจนเรื่อง targeted invalidation หลัง action อื่น ๆ |
---
## Business Rule Review
สิ่งที่สอดคล้อง:
- Employee เห็นเฉพาะข้อมูลตนเองผ่าน `getEmployeeIdsForUser()` และเงื่อนไข scope ใน [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts)
- ชั่วโมง approved ใช้ `approvedHours` ถ้ามี ไม่เช่นนั้น fallback ไป `hours`
- K / S / A คิดจากรายการที่อนุมัติแล้วเท่านั้น
- ปีที่ใช้กรองแยกจากค่า default ปีปัจจุบัน
จุดที่ควรระวัง:
- การใช้ปีอ้างอิงด้วย `new Date().getUTCFullYear()` และช่วงวันแบบ UTC อาจต่างจากการตีความปีตาม timezone ธุรกิจที่ใช้งานจริงปลายปี/ต้นปี
- หากพนักงานไม่มี employee profile ที่ผูกกับ user ระบบจะคืนค่าศูนย์เกือบทั้งหมด แต่ไม่ได้บอกผู้ใช้ว่าสาเหตุคือ “ยังไม่พบข้อมูลพนักงาน”
- ความคืบหน้าถูกจำกัดไม่เกิน 100% ทำให้ business outcome แบบ “เกินเป้า” ไม่ถูกสื่อสารบน UI
---
## Role & Permission Review
Employee
- View own dashboard: Pass
- View own statistics: Pass
- View own training history: Pass
- View notifications: Pass ผ่าน [src/app/dashboard/notifications/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/notifications/page.tsx)
- View announcements: Pass ผ่าน [src/app/dashboard/announcements/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/page.tsx)
- View other employees: Not observed in dashboard flow
- Approve training / edit policy / access HRD pages: Guarded by HRD-only routes
ข้อสังเกต:
- ฝั่ง route protection ถือว่าดี
- แต่ navigation ไม่สม่ำเสมอ: employee เข้า announcements และ notifications ได้โดยตรง แต่เมนูใน [src/config/nav-config.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts) ไม่ได้เปิดเส้นทางเหล่านี้ให้เห็นอย่างชัดเจนใน sidebar หลัก
---
## UI Review
จุดที่ดี:
- ใช้ `PageContainer`, `Card`, `Badge`, `Progress` และ chart shell เดียวกับโปรเจ็กต์
- spacing โดยรวมสม่ำเสมอ
- widget หลักแบ่งเป็น summary + content blocks อ่านง่ายบน desktop
จุดที่ควรปรับ:
- summary cards render `Badge` ทุกใบ แม้หลายใบส่ง `badge: ''` มา ทำให้มี badge ว่างหรือเหลือเพียง icon ใน [src/app/dashboard/overview/layout.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/layout.tsx)
- filter panel ใช้ native `<select>` แทน shared form/select patterns ของโปรเจ็กต์
- หน้า employee dashboard ไม่มี notification widget โดยตรง ทั้งที่มี notifications page อยู่แล้ว ทำให้ข้อมูลสำคัญถูกแยกบริบทออกจากหน้าหลัก
---
## UX Review
จุดที่ดี:
- ผู้ใช้เห็นภาพรวมข้อมูลสำคัญได้เร็ว
- ประกาศล่าสุดและกิจกรรมล่าสุดช่วยสร้างบริบทการใช้งานประจำวัน
จุดที่ควรปรับ:
- หากเกิด error ผู้ใช้จะเห็นข้อความเชิงเทคนิคเกินไป
- หากไม่มีข้อมูลเพราะยังไม่ผูก employee profile, ไม่มี policy, หรือยังไม่เคยส่งรายการ ระบบไม่อธิบายสาเหตุให้ชัด
- notifications แยกไปอีกหน้า แต่ไม่มีทางเข้าที่เด่นพอจาก dashboard/sidebar สำหรับ employee
- recent activities ใช้วันฝึกอบรมแสดงผล แต่ใช้เวลาสร้างรายการเป็นตัวเรียงลำดับ ซึ่งอาจขัดกับความคาดหวังว่า “ล่าสุด” หมายถึงฝึกเมื่อไร
---
## Accessibility Review
จุดที่ดี:
- ใช้ semantic button/link/card ของ shared UI หลายจุด
- chart components เปิด `accessibilityLayer` ใน Recharts หลายตัว
จุดที่ควรปรับ:
- error UI หลายจุดพึ่งพาสีและข้อความทั่วไป แต่ไม่มี recovery action ครบทุก slot
- native chart summary ไม่มี text alternative เชิงสรุปมากนักสำหรับ screen reader
- badge/progress สื่อสถานะหลักด้วย visual emphasis มากกว่าคำอธิบายเฉพาะกรณี “เกินเป้า”
---
## Code Quality Review
จุดที่ดี:
- รวม logic ฝั่ง dashboard ไว้ใน data builder เดียว ทำให้ trace business logic ได้ง่าย
- type ค่อนข้างชัด และ model ที่ส่งไป component แยกตามการใช้งานจริง
- ใช้ helper role กลาง เช่น `isOrganizationAdminRole()`
จุดที่ควรปรับ:
- `OverviewFilterPanel` เช็ก admin role ด้วย string comparison ตรง ๆ แทน helper กลาง
- มีการผสม concern ของ employee/admin dashboard ไว้ใน route เดียวค่อนข้างมาก
- naming บางส่วนยังติด template เดิม เช่น `RecentSales` ทั้งที่ใช้แสดงกิจกรรมการอบรม
---
## API Review
สิ่งที่พบ:
- Dashboard data ฝั่ง overview ดึงตรงจาก server helper ไม่ผ่าน client query เป็นหลัก
- Notifications ใช้ route handlers และ React Query ตาม pattern โปรเจ็กต์
- Notifications API validate query params ด้วย Zod
จุดที่ควรปรับ:
- error responses จาก API ถูกส่งกลับไปยัง UI แล้ว render `error.message` ตรง ๆ ในบางหน้าของ dashboard/notifications
- dashboard เองยังไม่มี contract ที่แยก concern เช่น announcements กับ notifications ให้ชัดในหน้าหลัก
---
## Performance Review
จุดที่ดี:
- `getOverviewDashboardData()` ถูกห่อด้วย `cache()` ช่วยลดงานซ้ำใน request เดียว
- widget หลักเป็น server-driven ลด client state ที่ไม่จำเป็น
จุดที่ควรระวัง:
- หน้า overview มีหลาย parallel route ที่ต่างเรียก data helper เดียวกัน แม้ `cache()` จะช่วย แต่โครงสร้างยังค่อนข้างซับซ้อนสำหรับ dashboard ที่ข้อมูลสัมพันธ์กันสูง
- chart และ card ทั้งหมดโหลดพร้อมกัน ไม่มี loading boundary ระดับหน้า root ของ overview
---
## Security Review
จุดที่ดี:
- route protection ฝั่ง employee/HRD ใช้งานจริง
- dashboard scope ข้อมูลตาม organization และ user
- notifications API จำกัดตาม `organizationId` และ `userId`
ความเสี่ยง:
- error boundary หลายจุดเปิดเผย `error.message` ตรง ๆ เช่น [src/app/dashboard/overview/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/error.tsx), [src/app/dashboard/overview/@sales/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@sales/error.tsx), [src/features/notifications/components/notifications-page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/components/notifications-page.tsx)
---
## Consistency Review
สอดคล้อง:
- ใช้ shared UI stack และ page guard ตามแนวทางโปรเจ็กต์
- notifications page ใช้ React Query + HydrationBoundary ตาม pattern กลาง
ไม่สอดคล้อง:
- dashboard table-like recent activity ใช้ custom list ซึ่งโอเคตามลักษณะข้อมูล แต่ naming ยังโยงกับ template เดิม
- filter panel ของ overview ยังไม่ใช้ shared select/form wrappers แบบที่หลาย feature ใช้อยู่
- employee-accessible pages บางหน้าไม่มีเมนูนำทางที่สอดคล้องกับสิทธิ์จริง
---
## Edge Case Review
- New employee: แสดงค่า 0 ได้ แต่ไม่บอกว่าเพราะยังไม่มีข้อมูลพนักงาน/ประวัติอบรม
- No training records: รองรับได้
- All completed: รองรับได้
- Over target: คำนวณได้ แต่ UI ไม่สื่อส่วนเกิน
- Missing category: K / S / A จะเหลือค่าตามข้อมูลที่มี แต่ไม่ได้อธิบาย category ที่หาย
- Missing policy: fallback เป็น 0 ได้ แต่ไม่มี warning/explanation
- Missing profile: ไม่พัง แต่ silent failure ในเชิง UX
- API failure: มี error UI แต่ข้อความไม่เหมาะกับผู้ใช้ปลายทาง
- Slow network: มี loading เฉพาะบาง slot แต่ไม่มี root loading ของ overview
---
## Problems Found
### High
- Description: Error UI หลายจุดแสดง raw `error.message` ต่อผู้ใช้โดยตรง
- File: [overview/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/error.tsx), [@sales/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@sales/error.tsx), [@area_stats/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@area_stats/error.tsx), [@pie_stats/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@pie_stats/error.tsx), [notifications-page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/components/notifications-page.tsx)
- Component: `OverviewError`, `SalesError`, `AreaStatsError`, `PieStatsError`, `NotificationsPage`
- Severity: High
- Impact: เสี่ยงเปิดเผยข้อมูลเชิงเทคนิคจาก backend/runtime และทำให้ UX ของผู้ใช้ปลายทางสับสน
- Recommendation: แสดงข้อความกลางที่ปลอดภัยต่อผู้ใช้ และ log รายละเอียดไว้ภายในแทน
- Description: Dashboard ไม่สื่อกรณี “เกินเป้าชั่วโมง” แม้ข้อมูลคำนวณถึงเป้าแล้ว
- File: [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts), [ksa-progress.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/ksa-progress.tsx)
- Component: `buildEmployeeCards`, `KsaProgress`
- Severity: High
- Impact: ผู้ใช้ที่ทำเกินเป้าจะเห็นเพียง 100% เท่ากับผู้ใช้ที่เพิ่งแตะเป้า ทำให้สูญเสียข้อมูลเชิงบริหารและแรงจูงใจ
- Recommendation: แยกสถานะ completed กับ exceeded และสื่อส่วนเกินบน UI
### Medium
- Description: Recent activities เรียงด้วย `createdAt` แต่แสดงวันที่เป็น `trainingDate`
- File: [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts), [recent-sales.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/recent-sales.tsx)
- Component: `getOverviewDashboardData`, `RecentSales`
- Severity: Medium
- Impact: ผู้ใช้อาจเข้าใจว่าเป็น “อบรมล่าสุดตามวันที่ฝึก” ทั้งที่จริงคือ “รายการที่สร้างล่าสุด”
- Recommendation: เลือกให้ชัดว่าจะเรียงตามวันฝึกหรือวันส่ง และทำ label ให้ตรง
- Description: พนักงานเข้าหน้า notifications และ announcements ได้ แต่ navigation หลักไม่สอดคล้องกับสิทธิ์จริง
- File: [nav-config.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts), [notifications/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/notifications/page.tsx), [announcements/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/page.tsx)
- Component: sidebar config, employee-accessible routes
- Severity: Medium
- Impact: discoverability ต่ำ ผู้ใช้เข้าฟีเจอร์ที่มีสิทธิ์ได้ยาก และ navigation model ไม่สม่ำเสมอ
- Recommendation: ทบทวนเมนู employee ให้ตรงกับ route ที่เปิดใช้งานจริง
- Description: Summary cards แสดง badge แม้ไม่มีข้อความ badge จริง
- File: [overview/layout.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/layout.tsx)
- Component: `OverViewLayout`
- Severity: Medium
- Impact: UI ดูไม่ตั้งใจและเพิ่ม noise ในส่วนสรุปสำคัญ
- Recommendation: render badge แบบมีเงื่อนไข หรือกำหนดข้อความ badge ให้ครบ
- Description: กรณี user ไม่มี employee profile / ไม่มี policy ระบบแสดงศูนย์อย่างเงียบ ๆ
- File: [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts)
- Component: `getOverviewDashboardData`
- Severity: Medium
- Impact: ผู้ใช้และทีม support แยกไม่ออกว่า “ไม่มีข้อมูล” หรือ “ข้อมูลเชื่อมไม่ครบ”
- Recommendation: เพิ่ม state หรือ messaging ที่อธิบายสาเหตุของข้อมูลว่าง
### Low
- Description: `OverviewFilterPanel` ใช้ string compare role ตรง ๆ แทน helper กลาง
- File: [overview-filter-panel.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/overview-filter-panel.tsx)
- Component: `OverviewFilterPanel`
- Severity: Low
- Impact: ลดความสม่ำเสมอของโค้ดและเพิ่มโอกาส logic drift
- Recommendation: ใช้ role helper กลางเดียวกับส่วนอื่น
- Description: ชื่อ component `RecentSales` ไม่ตรงกับหน้าที่จริง
- File: [recent-sales.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/recent-sales.tsx)
- Component: `RecentSales`
- Severity: Low
- Impact: อ่านโค้ดยากขึ้นและสะท้อนร่องรอยจาก template เดิม
- Recommendation: เปลี่ยนชื่อให้ตรงกับโดเมนเมื่อมีรอบ refactor
- Description: หน้า overview ไม่มี root loading state ของ route หลัก
- File: [src/app/dashboard/overview](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview)
- Component: overview route
- Severity: Low
- Impact: initial load ของทั้งหน้าอาจดูนิ่งเกินไปหาก data ช้า และพึ่ง loading ของบาง slot เท่านั้น
- Recommendation: เพิ่ม loading ระดับหน้าให้สอดคล้องกับหน้า dashboard สำคัญอื่น ๆ
---
## Missing Features
- Notification summary/widget บนหน้า dashboard สำหรับ employee
- ข้อความอธิบายกรณีไม่พบ employee profile หรือ policy เป้าหมาย
- การสื่อสารสถานะ “เกินเป้า” แยกจาก “ครบเป้า”
- Navigation entry ที่สอดคล้องกับ employee-accessible notifications/announcements
---
## Improvement Opportunities
- แยก employee dashboard semantics ให้ชัดกว่าการ reuse `overview` route ร่วมกับ HRD
- เพิ่ม domain-specific naming ให้ component/slot เพื่อให้อ่านโค้ดง่ายขึ้น
- ทำ filter panel ให้ใช้ shared form/select stack ของโปรเจ็กต์
- เพิ่ม explanatory empty states ตามสาเหตุจริงของข้อมูลว่าง
- เพิ่ม text summary สำหรับ charts เพื่อช่วยทั้ง accessibility และการอ่านค่าบน mobile
---
## Final Assessment
Architecture: 8/10
Functionality: 7.5/10
UI: 7/10
UX: 6.8/10
Accessibility: 6.8/10
Performance: 7.5/10
Security: 7.5/10
Maintainability: 7.6/10
Overall Score: 7.3/10

View File

@@ -0,0 +1,401 @@
# Employee Online Lessons Audit Report
## Executive Summary
Overall Status
- Needs Improvement
The employee-facing online lessons module follows the project's core architecture well: server-guarded dashboard routes, feature-local API contracts, React Query prefetch/hydration, and organization-scoped route handlers are all in place. Employee access to unpublished lessons is also enforced server-side, not only in the UI.
The main gaps are functional completeness and employee experience. The current implementation delivers a published lesson list and detail page, but the scope in `plans/audit-employee-online-lessons.md` expects more than content viewing. There is no employee progress tracking, no completion state, no learning history, no dashboard integration for lesson completion, no certificate section, no dedicated loading/error routes, and only basic attachment/video fallback handling. The module is structurally solid, but the employee learning workflow is still only partially implemented.
---
## Scope Reviewed
Routes
- `src/app/dashboard/online-lessons/page.tsx`
- `src/app/dashboard/online-lessons/[id]/page.tsx`
- `src/app/dashboard/online-lessons/manage/new/page.tsx`
- `src/app/dashboard/online-lessons/manage/[id]/page.tsx`
- `src/app/api/online-lessons/route.ts`
- `src/app/api/online-lessons/[id]/route.ts`
Feature components and contracts
- `src/features/online-lessons/components/online-lessons-listing.tsx`
- `src/features/online-lessons/components/online-lessons-page.tsx`
- `src/features/online-lessons/components/online-lesson-detail-page.tsx`
- `src/features/online-lessons/components/online-lesson-form.tsx`
- `src/features/online-lessons/components/online-lesson-action-menu.tsx`
- `src/features/online-lessons/api/types.ts`
- `src/features/online-lessons/api/service.ts`
- `src/features/online-lessons/api/queries.ts`
- `src/features/online-lessons/api/mutations.ts`
- `src/features/online-lessons/server/online-lesson-data.ts`
- `src/features/online-lessons/schemas/online-lesson.ts`
Shared/auth/navigation/storage
- `src/lib/auth/page-guards.ts`
- `src/lib/auth/session.ts`
- `src/lib/auth/roles.ts`
- `src/lib/online-lesson-storage.ts`
- `src/config/nav-config.ts`
- `docs/AI_DEVELOPMENT_GUIDE.md`
- `docs/PROJECT_ARCHITECTURE.md`
---
## Functional Review
| Feature | Status | Notes |
| ------- | ------ | ----- |
| Lesson list loads | Pass | `page.tsx` and `online-lessons-listing.tsx` use server prefetch + `HydrationBoundary`, then `useSuspenseQuery()` in `online-lessons-page.tsx`. |
| Pagination | Pass | Backed by `page` and `limit` params and returned from `/api/online-lessons`. |
| Search | Pass | Employee list supports text search through query state and server filtering. |
| Category filter | Pass | Employee list supports category filtering; categories are scoped server-side. |
| Sorting | Partial | Server sorts by published status and publish/create dates, but there is no employee-facing sort control. |
| Responsive cards | Fail | The employee list is a `DataTable`, not lesson cards. |
| Empty state | Pass | The page renders a clear empty state with filter reset support. |
| Loading state | Partial | There is inline fetch feedback in the list, but no route-level `loading.tsx` for list or detail. |
| Error state | Fail | No route-level `error.tsx` exists under `src/app/dashboard/online-lessons`. |
| Lesson detail basics | Partial | Title, description, category, published date, thumbnail, and attachment links exist. Instructor and duration do not. |
| Uploaded video support | Pass | Route validation and detail rendering support uploaded MP4, WebM, and MOV. |
| YouTube support | Pass | Detail page converts standard, short, shorts, and embed URLs into iframe embeds. |
| Video fallback UI | Partial | Non-embeddable external URLs fall back to an open-link card, but uploaded-video failure handling is minimal. |
| Attachment handling | Partial | Open/download links exist, but there is no inline image or PDF preview. |
| Certificate section | Fail | No certificate field or UI exists in the employee lesson detail. |
| Progress display | Fail | No lesson progress model, API field, or UI is present. |
| Completed status | Fail | No completion badge or completion tracking exists. |
| Learning history | Fail | No viewed/completed/recent lesson history is implemented. |
| Dashboard integration | Fail | No evidence that online lesson completion feeds employee dashboard metrics or training hours. |
---
## Business Rule Review
What is aligned
- Employee lesson pages use `requireEmployeeDashboardAccess()` or the organization-scoped API layer before rendering or returning data.
- Employee lesson queries are filtered to published lessons only through `listOnlineLessonsForAccess()` using `eq(onlineLessons.isPublished, true)` for non-HRD users.
- Direct detail access is also server-scoped through `getOnlineLessonByIdForAccess()`, so unpublished lessons are not exposed by URL guessing alone.
- Create, update, delete, and publish/archive actions are protected by `requireHRD()` in route handlers and `requireHRDDashboardAccess()` in manage pages.
Inconsistencies or gaps
- The business rule protection for employee visibility depends on `isPublished` being kept in sync with `status`. The implementation does keep them aligned today, but the access rule is coupled to that derived flag instead of checking `status === 'published'` directly.
- The audit scope expects progress, completion, history, dashboard updates, training-hour rules, and certificate visibility rules, but there is no runtime contract for those concepts anywhere in the online lessons feature.
---
## Role & Permission Review
Employee permissions
- View lesson list: Pass
- View lesson detail: Pass
- Watch embedded or uploaded video: Pass
- Search lessons: Pass
- Filter by category: Pass
- Download allowed files: Pass
- Create/edit/delete/publish lessons: Guarded server-side and route-side
- Access HRD manage pages: Guarded
- Access hidden or draft lessons directly: Guarded server-side
HRD/Admin permissions
- Manage pages are limited to HRD through `requireHRDDashboardAccess()`.
- Mutating API endpoints are limited to HRD through `requireHRD()`.
Notes
- UI permission separation is generally clean. Employee list rows only show a single "open lesson" action, while HRD users additionally see status/creator columns and the action menu.
- The main employee route also exposes the "add online lesson" CTA when the signed-in user is HRD. This is reasonable for a shared route, but it means the page mixes employee and management affordances in one surface.
---
## UI Review
Strengths
- The page uses `PageContainer`, card sections, and shared dashboard primitives consistent with the rest of the app.
- Thumbnail, metadata, badges, and buttons are visually consistent with the existing design system.
- Empty state handling is clearer than many legacy areas because search/filter reset remains available.
Weaknesses
- The employee list is table-centric, while the plan explicitly calls for responsive lesson cards. On mobile, this is usable but less content-first than a card layout.
- The detail page lacks dedicated sections for instructor, duration, progress, completion, certificate availability, or related learning context.
- Attachment UX is basic. Users can open/download files, but there is no preview affordance for images or PDFs.
- There is no loading skeleton or route-level loading view for the employee route family.
---
## UX Review
Strengths
- Discovery is straightforward for published lessons: search, category filter, and a single row CTA keep the main flow simple.
- Detail-page video behavior covers the common YouTube URL shapes and also supports uploaded files.
Weaknesses
- The module currently behaves more like a content library than a learning workflow.
- Users do not get progress feedback, watched/completed state, or "continue where you left off" guidance.
- Missing video, broken uploaded video, and broken embed cases do not provide especially strong recovery guidance.
- The detail page exposes creator/status metadata but omits learner-oriented metadata such as duration and completion cues.
---
## Accessibility Review
What is good
- Buttons and links are based on shared UI primitives, so baseline keyboard and focus behavior should be reasonable.
- Thumbnail images include `alt` text derived from lesson title.
- Native video controls are available for uploaded videos.
Risks
- The `<track kind="captions" />` element in the uploaded-video player has no `src`, so captions are not actually implemented.
- There is no explicit accessibility treatment for attachment previews because previews do not exist yet.
- Video error states and fallback messaging are limited, which can leave assistive users with less context when media fails.
---
## Code Quality Review
Strengths
- The feature follows the expected app-owned structure with `api/types.ts`, `service.ts`, `queries.ts`, `mutations.ts`, schema validation, and server data helpers.
- Route handlers keep persistence and authorization logic on the server.
- Query invalidation is simple and predictable through `onlineLessonKeys`.
Weaknesses
- `online-lessons-page.tsx` is doing list rendering, filters, columns, empty state, and role-based behavior in one client component, which makes the employee list surface larger than necessary.
- The employee-facing feature has no shared learner-progress contracts, so expanding the scope later will likely require contract changes across types, routes, and UI together.
- There is no shared video/attachment display component even though the plan suggests richer media behavior that may need reuse.
---
## API Review
Strengths
- `/api/online-lessons` and `/api/online-lessons/[id]` enforce organization access and HRD-only mutation rules.
- List endpoint supports pagination, search, category filtering, and status filtering for managers.
- File validation for video, attachment, and thumbnail uploads is handled server-side before storage.
Weaknesses
- Employee-facing API contracts do not include progress, completion, last-viewed, certificate, duration, or instructor data.
- There is no separate endpoint for attachment download authorization; attachments are exposed by stored file URL once the lesson payload is returned.
- Video and attachment runtime errors are left mostly to browser-native behavior instead of richer API/UI recovery states.
---
## Security Review
Strengths
- Route handlers use shared auth helpers instead of trusting client state.
- Employee direct access to hidden/draft lessons is blocked in both list and detail server queries.
- HRD-only mutations are enforced in API handlers and manage pages.
- Storage helper code validates file types, size limits, and upload-path safety.
Risks
- Lesson assets appear to be served from public upload URLs. That is convenient, but it means asset access is not re-authorized per request once a URL is known.
- The access policy is derived from `isPublished`; if future code ever desynchronizes `status` and `isPublished`, employee visibility could drift from the intended business rule.
---
## Performance Review
Strengths
- List and detail pages use server prefetch + React Query hydration.
- Pagination is server-driven, which keeps the list payload bounded.
- Detail pages fetch only one lesson at a time.
Weaknesses
- The detail page uses raw `<img>` instead of `next/image`, so thumbnail optimization is limited.
- Uploaded video elements do not appear to use lazy or staged loading strategies beyond browser defaults.
- There is no dedicated progressive loading UX for media-heavy content on slow networks.
---
## Data Validation Review
What is covered
- Title is required through `onlineLessonSchema`.
- Status is restricted by schema.
- Video URL must be an `http` or `https` URL when provided.
- Route handlers enforce that at least one video source exists on create and retained update flows.
- File type and file size checks exist for uploaded video, attachment, and thumbnail files.
What is missing
- No validation exists for instructor, duration, certificate rules, or learner progress because those fields are absent from the feature model.
- No YouTube-specific validation exists beyond generic URL parsing and best-effort embed conversion.
---
## Edge Case Review
- No lessons: handled with an empty state.
- One lesson: should work cleanly with the current list/detail flow.
- Many lessons: pagination exists, but the table-first UI is less optimized for content browsing than cards.
- Missing thumbnail: handled with a placeholder block on the detail page.
- Missing video: handled with a "no video" message.
- Broken YouTube link: partial handling only; non-embeddable links get a fallback card, but broken embeds still depend on iframe/browser behavior.
- Broken uploaded video: partial handling only; there is no custom error state or recovery UI.
- Missing attachment: handled by omitting the attachment section.
- Invalid PDF/image preview: not applicable because inline previews are not implemented.
- Slow network: partial handling only; fetch feedback exists, but there are no dedicated loading routes or skeletons.
- API failure: no dedicated route error boundary exists for the employee online-lessons routes.
- Unauthorized direct access to hidden lessons: guarded server-side.
- Archived lesson via direct URL: guarded as long as `isPublished` remains false for archived lessons.
---
## Consistency Review
Consistent with project patterns
- Uses `PageContainer`, local feature contracts, route handlers, Drizzle-backed server helpers, React Query prefetch, and shared auth/page guard helpers.
- Reuses shared dashboard UI primitives and badges.
- Follows the feature-module architecture documented in `docs/AI_DEVELOPMENT_GUIDE.md` and `docs/PROJECT_ARCHITECTURE.md`.
Not fully consistent
- The list UI uses `DataTable`, but it does not reuse the richer shared `DataTableToolbar` pattern described in the project guide.
- The employee online lesson experience is much lighter than nearby employee-facing areas such as dashboard and training records, especially around loading/error states and learner feedback.
---
## Problems Found
### Critical
- None found.
### High
- Description: The employee online lessons feature has no implementation for progress tracking, completion state, learning history, certificate availability, or dashboard integration, even though these are core audit-scope expectations.
- File: `src/features/online-lessons/api/types.ts`, `src/features/online-lessons/components/online-lessons-page.tsx`, `src/features/online-lessons/components/online-lesson-detail-page.tsx`, `src/app/api/online-lessons/route.ts`, `src/app/api/online-lessons/[id]/route.ts`
- Component: API contracts, employee list page, employee detail page, list/detail route handlers
- Severity: High
- Impact: The module supports browsing lessons, but not the employee learning lifecycle expected by the plan. Downstream reporting and dashboard experiences cannot reflect lesson completion because the feature model does not capture it.
- Evidence: `serializeOnlineLesson()` only returns content/admin metadata in `src/features/online-lessons/server/online-lesson-data.ts:31`; there are no progress/completion fields in `src/features/online-lessons/api/types.ts`; employee list/detail components render no such state.
- Recommendation: Define learner-state contracts first, then add server-side tracking, employee UI surfaces, and dashboard/report integration on top of that contract.
### Medium
- Description: There are no dedicated `loading.tsx` or `error.tsx` route files for the employee online-lessons routes.
- File: `src/app/dashboard/online-lessons`
- Component: employee list and detail route tree
- Severity: Medium
- Impact: Slow-network and failure states are less consistent with the rest of the dashboard and provide weaker recovery UX.
- Evidence: No `loading.tsx` or `error.tsx` files exist under `src/app/dashboard/online-lessons`; list page only shows inline fetching text and detail relies on default route behavior.
- Recommendation: Add route-level loading and error surfaces consistent with the app's employee-facing patterns.
- Description: The employee lesson detail page does not include instructor, duration, certificate, or progress-oriented sections, while it does expose management-oriented metadata such as creator name and publication status.
- File: `src/features/online-lessons/components/online-lesson-detail-page.tsx`
- Component: `OnlineLessonDetailPage`
- Severity: Medium
- Impact: The page gives weaker learning context than expected and prioritizes admin metadata over learner metadata.
- Evidence: The metadata block renders `status`, `category`, `published_at`, and `created_by_name` around `src/features/online-lessons/components/online-lesson-detail-page.tsx:111`, but no instructor/duration/progress/certificate fields exist.
- Recommendation: Rebalance the detail layout around employee learning needs and move non-essential admin metadata out of the primary reading flow.
- Description: Attachment handling is limited to link-based open/download actions and does not implement inline image preview, PDF preview, or unsupported-file guidance.
- File: `src/features/online-lessons/components/online-lesson-detail-page.tsx`
- Component: attachment section
- Severity: Medium
- Impact: Learners have less confidence about what a file contains before opening it, and the module does not meet the richer attachment scope in the audit plan.
- Evidence: The attachment block only renders file name plus open/download buttons around `src/features/online-lessons/components/online-lesson-detail-page.tsx:208`.
- Recommendation: Add file-type-aware previews and clearer unsupported-file messaging where preview is not possible.
- Description: Uploaded-video failure handling is minimal, and the captions track is not actually configured.
- File: `src/features/online-lessons/components/online-lesson-detail-page.tsx`
- Component: uploaded video player
- Severity: Medium
- Impact: Broken media leads to a weak recovery path, and caption accessibility is not functional.
- Evidence: The uploaded-video block uses native `<video controls>` with a blank `<track kind="captions" />` around `src/features/online-lessons/components/online-lesson-detail-page.tsx:158`.
- Recommendation: Add explicit media error UI and only render captions tracks when real caption files exist.
- Description: Lesson assets are served from public upload URLs rather than protected download endpoints.
- File: `src/lib/online-lesson-storage.ts`, `src/features/online-lessons/server/online-lesson-data.ts`
- Component: lesson asset storage and serialized payload
- Severity: Medium
- Impact: Once a client receives a file URL, asset access is no longer re-checked per request.
- Evidence: Serialized lesson payload includes `video_file_url`, `attachment_url`, and `thumbnail_url` from storage-backed public paths in `src/features/online-lessons/server/online-lesson-data.ts:38-43`.
- Recommendation: If the business wants stricter asset protection, move file delivery behind authorized download endpoints or signed URLs.
### Low
- Description: The employee list experience is table-based rather than card-based, which is less aligned with content browsing and the audit plan's responsive-card expectation.
- File: `src/features/online-lessons/components/online-lessons-page.tsx`
- Component: `OnlineLessonsPage`
- Severity: Low
- Impact: Usability is acceptable, but the presentation is less content-first on mobile and less aligned with lesson-library expectations.
- Evidence: The list renders a `DataTable` with columns and row CTAs rather than lesson cards.
- Recommendation: Consider a dedicated employee card layout if lesson discovery and media browsing become a priority.
---
## Missing Features
- Lesson progress display
- Completed lesson state
- Remaining lessons or progress calculation
- Completion badge
- Learning history for viewed/completed/recent lessons
- Dashboard integration for lesson completion or training-hour impact
- Certificate availability section
- Instructor metadata
- Duration metadata
- Attachment previews for images and PDFs
- Dedicated route-level loading UI
- Dedicated route-level error UI
- Stronger broken-video fallback and recovery UX
---
## Improvement Opportunities
- Separate the employee learning experience from the HRD management experience more clearly, especially in detail-page metadata and list presentation.
- Introduce learner-state contracts at the API layer before adding UI for progress/history/completion.
- Reuse a richer shared toolbar/filter pattern if the employee list gains more discovery controls.
- Consider protected delivery for lesson assets if access revocation or stricter confidentiality becomes important.
- Extract video and attachment presentation into reusable components if the feature expands to richer media states.
---
## Final Assessment
Architecture: 8.2/10
Functionality: 6.7/10
Role & Permission: 8.4/10
UI: 7.0/10
UX: 6.6/10
Accessibility: 6.8/10
Performance: 7.4/10
Security: 7.7/10
Maintainability: 7.8/10
Overall Score: 7.3/10

View File

@@ -0,0 +1,318 @@
# Employee Training History Audit Report
## Summary
Overall Status
- Good
ภาพรวม:
- หน้า `Training Records` เดินตามโครงสร้างหลักของโปรเจ็กต์ค่อนข้างดี โดยใช้ `PageContainer`, shared `DataTable`, React Query, local route handlers, และ server-side organization scoping ตามแพตเทิร์นของ `training-records`
- ฝั่ง server มีการบังคับสิทธิ์หลักถูกต้อง ทั้ง list, detail, create, update, delete และ review โดย employee เห็นได้เฉพาะข้อมูลของตนเอง ส่วน HRD/admin เห็นได้ระดับ organization
- จุดที่ควรปรับหลักอยู่ที่ความสอดคล้องระหว่าง UI กับ permission จริง, ความชัดเจนของ detail/edit flow, และการเปิดใช้ capability ฝั่ง UI ให้ครบกับ business rule เรื่อง historical filtering
---
## Architecture Review
สิ่งที่พบ:
- route หลักอยู่ที่ `src/app/dashboard/training-records/page.tsx`
- list page ใช้ server prefetch ผ่าน `src/features/training-records/components/training-record-listing.tsx`
- table ใช้ shared `DataTable`, `DataTableToolbar`, `useDataTable`
- data source ใช้ `trainingRecordsQueryOptions()` -> `api/service.ts` -> `/api/training-records`
- persistence และ access control อยู่ที่ `src/app/api/training-records/route.ts`, `src/app/api/training-records/[id]/route.ts`, และ `src/features/training-records/server/training-record-data.ts`
- detail route reuse form page ผ่าน `src/features/training-records/components/training-record-view-page.tsx`
ข้อสรุป:
- โครงสร้างโดยรวมสอดคล้องกับเอกสาร `AI_DEVELOPMENT_GUIDE` และ canonical feature ของโปรเจ็กต์
- อย่างไรก็ดี semantic ของหน้า detail/edit ยังไม่ชัด เพราะ route เดียวกันถูกใช้ทั้ง "ดูรายละเอียด" และ "แก้ไข"
---
## Functional Review
| Feature | Status | Notes |
| ------- | ------ | ----- |
| View own training history | Pass | page ใช้ `requireEmployeeDashboardAccess()` และ list query ถูก scope ฝั่ง server |
| Search records | Pass | รองรับ text search ผ่าน table toolbar |
| Filter by status | Pass | UI และ API ทำงานสอดคล้องกัน |
| Filter by training type | Pass | UI และ API ทำงานสอดคล้องกัน |
| Sort records | Pass | default sort ตาม `training_date desc` และรองรับ server-side sort |
| Pagination | Pass | ใช้ shared DataTable pagination |
| Create training record | Pass | ใช้ TanStack Form, validation, mutation, และ certificate upload ต่อเนื่อง |
| Edit pending record | Partial | server guard ถูกต้อง แต่ client affordance กว้างกว่า permission บางกรณี |
| Delete pending record | Partial | server guard ถูกต้อง แต่ client affordance กว้างกว่า permission บางกรณี |
| View detail vs update flow | Partial | action `Detail` และ `Update` พาไป route เดียวกัน |
| Historical filtering by year | Partial | backend รองรับ `year` filter แต่หน้า employee training history ยังไม่ expose |
| Loading state | Pass | มี `loading.tsx` ทั้ง list และ detail |
| Error state | Partial | list/detail route ไม่มี dedicated error boundary และ review error page แสดง raw error |
| Empty state | Partial | มี empty state แต่ยังไม่อธิบายกรณี employee profile ไม่ถูกผูกกับ user |
---
## Business Rule Review
สิ่งที่สอดคล้อง:
- employee เห็นเฉพาะ record ของตนเองผ่าน `buildTrainingRecordAccessClause()` และ `getEmployeeIdsForUser()`
- HRD/admin เห็นข้อมูลได้ทั้ง organization
- employee แก้ไขหรือลบได้เฉพาะ record ที่ยัง `pending` ตาม server rule `canEmployeeManagePendingRecord()`
- duration ถูก normalize และ serialize สม่ำเสมอผ่าน minute-based helpers
จุดที่ควรระวัง:
- business rule เรื่องการแยกประวัติเป็นรายปีมี support ฝั่ง API (`year`) แต่หน้า employee training history ยังไม่เปิดให้ใช้จาก UI
- หาก record เป็น `pending` แต่ไม่มี `user_id` ผูกกับผู้ใช้งานปัจจุบัน UI ยังดูเหมือนแก้ไขได้ ทั้งที่ API จะปฏิเสธ
---
## Role & Permission Review
Employee
- View own list: Pass
- View own detail: Pass
- Create own record: Pass
- Edit own pending record: Partial
- Delete own pending record: Partial
- Review records: Guarded
- View other employees: Guarded server-side
HRD / Admin
- View organization-scoped training records: Pass
- Review records: Pass
- Create/update/delete records: Pass ตาม role model ปัจจุบัน
ข้อสังเกต:
- ฝั่ง server permission ค่อนข้างแข็งแรง
- ฝั่ง UI มี permission drift บางจุด เพราะ action menu และ form ใช้เงื่อนไข `approval_status === 'pending'` เป็นหลัก มากกว่าการยืนยัน ownership แบบเดียวกับ API
---
## UI Review
จุดที่ดี:
- ใช้ shared DataTable stack สม่ำเสมอ
- list page responsive ในระดับดี และจำกัด overflow ไว้ใน table shell
- loading skeleton ของ list/detail ชัดเจนและสอดคล้องกับหน้า dashboard อื่น
จุดที่ควรปรับ:
- action `Detail` กับ `Update` ใช้ปลายทางเดียวกัน ทำให้ label กับพฤติกรรมไม่ตรงกัน
- empty state เป็น generic "ยังไม่มีประวัติ" โดยไม่แยกกรณี "ยังไม่ผูก employee profile"
- detail route แสดงเป็นฟอร์มเต็มทันที แม้ผู้ใช้เพียงต้องการอ่านข้อมูล
---
## UX Review
จุดที่ดี:
- flow สร้าง record ใหม่ตรงไปตรงมา
- หลัง create/update มี toast และ redirect กลับ list
- search/filter/sort อยู่ใน pattern เดียวกับ feature อื่น ทำให้คาดเดาได้
จุดที่ควรปรับ:
- ผู้ใช้บางรายอาจกด `Update` หรือ `Delete` ได้จาก menu แต่ไปชน 403 ตอนเรียก API จริง
- ไม่มีความต่างชัดเจนระหว่าง "ดูรายละเอียด" กับ "แก้ไข"
- หน้า list ยังขาด filter รายปี ทั้งที่บริบทธุรกิจของ training history มักอิงปีปฏิทิน
---
## Accessibility Review
จุดที่ดี:
- action menu trigger มี `sr-only` label
- ฟิลด์หลักในฟอร์มมี label และ `aria-invalid`
- ปุ่มหลักและ loading skeleton อยู่ในโครงสร้างที่อ่านได้
จุดที่ควรระวัง:
- ความต่างระหว่าง badge status ยังพึ่งข้อความมากกว่า visual emphasis
- การใช้ route เดียวกันสำหรับ detail/edit อาจทำให้ mental model ของผู้ใช้และ assistive flow สับสน
---
## Code Quality Review
จุดที่ดี:
- แยก `types.ts`, `service.ts`, `queries.ts`, `mutations.ts`, `server` helpers ชัดเจน
- route handlers บางและส่งงาน query logic ไปที่ feature server helper
- ใช้ shared form/table stack ตาม guideline
จุดที่ควรสังเกต:
- permission logic ฝั่ง client ซ้ำแต่ไม่เทียบเท่ากับ server rule
- naming ของ detail/update flow ยังไม่สะท้อน behavior จริง
---
## API Review
จุดที่ดี:
- contracts ของ list/detail ชัด
- route handlers ใช้ `requireOrganizationAccess()` และคืนค่า error ที่สม่ำเสมอพอสมควร
- list endpoint รองรับ filter มากกว่าที่ UI ปัจจุบันใช้
จุดที่ควรปรับ:
- client surface ยังใช้ filter contract ไม่ครบ โดยเฉพาะ `year`
- review error UI แสดง `error.message` ตรง ๆ ต่อผู้ใช้
---
## Performance Review
จุดที่ดี:
- list page ใช้ server prefetch + hydration
- table ใช้ server-side pagination/sorting/filtering
- detail page prefetch เฉพาะ query ที่เกี่ยวข้อง
จุดที่ควรระวัง:
- list query สร้าง certificate preview map ให้ทุก row ซึ่งสมเหตุผลกับคอลัมน์ปัจจุบัน แต่ถ้าอนาคตมี lightweight employee view แยกจาก management view อาจแยก payload ได้อีก
---
## Security Review
จุดที่ดี:
- page ใช้ route guard ก่อน render
- API ใช้ organization access check และ server-side record scoping
- update/delete บังคับ pending-only และ ownership เฉพาะ employee
ความเสี่ยง:
- review error page แสดง raw runtime error ต่อผู้ใช้
---
## Consistency Review
สอดคล้อง:
- ใช้ `PageContainer`
- ใช้ shared DataTable stack
- ใช้ TanStack Form + project wrappers
- ใช้ route handlers + Drizzle ตาม pattern กลาง
ไม่สอดคล้อง:
- detail page ของ training history ยังไม่แยก read-only view ออกจาก edit semantics
- UI permission checks ฝั่ง client ยังไม่ align กับ server helper เดียวกัน
---
## Edge Case Review
- New employee with no history: รองรับได้ แต่ empty state ยัง generic
- Employee without linked profile: create flow block ได้ แต่ list/empty state ไม่อธิบายสาเหตุ
- Pending record without linked `user_id`: มีความเสี่ยงด้าน UX เพราะ UI อาจเปิด action แต่ API ปฏิเสธ
- Approved record: server กัน edit/delete ถูกต้อง
- No certificate: รองรับได้
- API failure: review route มี error UI แต่เปิด raw error; list/detail ไม่มี dedicated boundary
- Slow network: loading skeleton มีให้ทั้ง list และ detail
---
## Problems Found
### High
- Description: Client-side action และ form affordance อนุญาตให้ employee เห็น `Update/Delete` จากเงื่อนไข `approval_status === 'pending'` แม้ API จะอนุญาตเฉพาะ record ที่ `recordUserId === actorUserId`
- File: `src/features/training-records/components/training-record-tables/cell-action.tsx`, `src/features/training-records/components/training-record-form.tsx`, `src/features/training-records/server/training-record-data.ts`, `src/app/api/training-records/[id]/route.ts`
- Component: `CellAction`, `TrainingRecordForm`, `canEmployeeManagePendingRecord`, `PATCH/DELETE /api/training-records/[id]`
- Severity: High
- Impact: ผู้ใช้สามารถเข้าสู่ flow แก้ไข/ลบได้ แต่ลงท้ายด้วย 403 เมื่อ record เป็น pending ที่ยังไม่ผูก `user_id` อย่างครบถ้วน ทำให้ permission model ใน UI กับ backend ไม่ตรงกัน
- Recommendation: ให้ client reuse permission contract เดียวกับ server หรือส่ง capability flag จาก API เพื่อไม่ให้ action menu/submit state over-promise
### Medium
- Description: หน้า employee training history ยังไม่ expose `year` filter ทั้งที่ API, types, และ server helper รองรับแล้ว
- File: `src/app/dashboard/training-records/page.tsx`, `src/features/training-records/components/training-record-listing.tsx`, `src/features/training-records/components/training-record-tables/index.tsx`, `src/features/training-records/api/types.ts`, `src/app/api/training-records/route.ts`
- Component: page route, listing page, `TrainingRecordsTable`, route handler contract
- Severity: Medium
- Impact: ผู้ใช้ยังแยกดูประวัติรายปีจากหน้า training history ไม่ได้ ทั้งที่เป็น use case ธุรกิจหลักของประวัติการอบรม
- Recommendation: เปิด filter รายปีใน UI และเชื่อมกับ query state เดิมของหน้า
- Description: `Detail` และ `Update` action พาไป route เดียวกัน และ route นั้น render ฟอร์มแก้ไข/อ่านแบบเดียวกัน
- File: `src/features/training-records/components/training-record-tables/cell-action.tsx`, `src/features/training-records/components/training-record-view-page.tsx`
- Component: `CellAction`, `TrainingRecordViewPage`
- Severity: Medium
- Impact: semantics ของหน้ารายละเอียดไม่ชัด ผู้ใช้ไม่สามารถแยกได้ว่าเข้ามาเพื่ออ่านหรือเพื่อแก้ไข และ flow นี้ทำให้ UX ของ training history ดูคลุมเครือ
- Recommendation: แยก read-only detail state ให้ชัด หรือปรับ label/action ให้ตรงพฤติกรรมจริง
- Description: list/detail route ของ training history ไม่มี dedicated error boundary ขณะที่ review error page แสดง raw `error.message`
- File: `src/app/dashboard/training-records`, `src/app/dashboard/training-records/[trainingRecordId]/review/error.tsx`
- Component: training history route, `TrainingRecordReviewError`
- Severity: Medium
- Impact: เมื่อ query/render พัง ผู้ใช้ได้ UX ที่ไม่สม่ำเสมอ และบางกรณีอาจเห็นข้อความเทคนิคจาก runtime
- Recommendation: เพิ่ม route-level error UI ที่ปลอดภัยและใช้ข้อความสำหรับผู้ใช้ปลายทาง
- Description: empty state ของหน้า list ยังไม่แยกกรณี "ไม่มีประวัติ" ออกจาก "บัญชียังไม่ถูกผูกกับ employee profile"
- File: `src/features/training-records/components/training-record-tables/index.tsx`, `src/features/training-records/components/training-record-form.tsx`
- Component: `TrainingRecordsTable`, `TrainingRecordForm`
- Severity: Medium
- Impact: ผู้ใช้และทีม support แยกสาเหตุของข้อมูลว่างได้ยาก
- Recommendation: เพิ่ม explanatory state หรือ guidance สำหรับบัญชีที่ยังไม่ผูก employee profile
### Low
- Description: status badge ใน table ใช้ `outline` เหมือนกันทุกสถานะ
- File: `src/features/training-records/components/training-record-tables/columns.tsx`
- Component: `columns`
- Severity: Low
- Impact: การสแกนสถานะด้วยสายตาทำได้ช้ากว่าที่ควร
- Recommendation: เพิ่ม visual distinction ระหว่าง `pending`, `approved`, `rejected`, `needs_revision`
---
## Missing Features
- filter รายปีสำหรับ employee training history
- dedicated error UI สำหรับ list/detail route
- explanatory empty state สำหรับบัญชีที่ยังไม่ผูก employee profile
- clear separation ระหว่าง read-only detail กับ edit flow
---
## Improvement Opportunities
- ส่ง permission/capability flags จาก API เพื่อให้ UI action menu ตรงกับ server policy เสมอ
- แยก employee-facing training history semantics ออกจาก management/edit semantics ให้ชัดขึ้น
- พิจารณาเพิ่ม lightweight read-only summary row/detail state สำหรับ employee
- ปรับ labels และ CTA ให้สะท้อน flow จริง เช่น detail, edit, review
---
## Final Assessment
Architecture: 8.2/10
Functionality: 7.4/10
UI: 7.2/10
UX: 6.9/10
Accessibility: 7.0/10
Performance: 7.8/10
Security: 8.0/10
Maintainability: 7.5/10
Overall Score: 7.5/10

View File

@@ -0,0 +1,297 @@
# Pending Review Audit Report
## Summary
Overall Status
- Good
ภาพรวม:
- หน้า `Pending Review` มีโครงสร้างค่อนข้างดี ใช้ shared DataTable stack, React Query, route handler, server-side guard และ validation ตาม pattern ของโปรเจกต์
- มี loading state, error state, empty state, pagination, search, sorting, filtering และ review flow ครบในระดับใช้งานจริง
- อย่างไรก็ตาม ยังมีจุดสำคัญที่ทำให้ semantics ของหน้าไม่คมพอ โดยเฉพาะเรื่องหน้า `Pending Review` ยังไม่ได้บังคับให้เป็น “pending only” จริงทั้ง flow
---
## Architecture Review
สิ่งที่พบ:
- Route หลักใช้ [src/app/dashboard/pending-review/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/pending-review/page.tsx)
- หน้า list ใช้ server prefetch ผ่าน [src/features/training-records/components/pending-review-listing.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-listing.tsx)
- table ใช้ shared `DataTable`, `DataTableToolbar`, `useDataTable`
- data source ใช้ feature เดียวกับ `training-records` ผ่าน `trainingRecordsQueryOptions()`
- review action ใช้ dedicated route `/dashboard/training-records/[trainingRecordId]/review`
- API ใช้ route handler และ Drizzle
- ไม่พบ Prisma หรือ Server Actions ใน flow นี้
ข้อสรุป:
- สถาปัตยกรรมโดยรวมสอดคล้องกับมาตรฐานโปรเจกต์
- จุดที่ยังไม่คมคือหน้า `pending-review` ยัง reuse list endpoint แบบ generic มากกว่าจะมี scope ที่ชัดว่าเป็น review queue โดยตรง
---
## UI Review
จุดที่ดี:
- ใช้ `PageContainer` ถูกต้อง
- ปุ่ม action หน้า page header และปุ่มในฟอร์มมี responsive behavior พอใช้
- ตารางใช้ shell กลางของโปรเจกต์ ทำให้ spacing และ pagination ค่อนข้างสม่ำเสมอ
- action menu ในตารางมีขนาดพอเหมาะและมี `aria-label`
จุดที่ควรระวัง:
- filter `company`, `department`, `year` เป็น text input ทั้งหมด ทำให้ UX ไม่แน่นเท่า faceted/select filter
- badge status ใช้ `variant='outline'` เหมือนกันทุกสถานะ ทำให้ความแตกต่างทางสายตาระหว่าง `pending`, `approved`, `rejected`, `needs_revision` ไม่ชัด
- empty state ไม่มีทางลัดให้ล้าง filter จากหน้าจอเดียว
---
## UX Review
จุดที่ดี:
- หากไม่มี `status` ใน URL จะ redirect ไป `?status=pending`
- มีเส้นทางกลับไปหน้ารายการอบรมหลัก
- review form หลัง submit สำเร็จจะ toast, redirect กลับ queue, และ refresh ข้อมูล
จุดที่ควรปรับ:
- ผู้ใช้ยังสามารถเปลี่ยน filter status ไปดู `approved`, `rejected`, `needs_revision` บนหน้า `Pending Review` ได้ ทำให้ชื่อหน้ากับพฤติกรรมจริงไม่ตรงกัน
- เมื่อกรองแล้วไม่เจอข้อมูล ผู้ใช้ไม่เห็น toolbar และไม่เห็นปุ่ม reset filter
- error boundary แสดง `error.message` ตรง ๆ ซึ่งอาจไม่เหมาะกับผู้ใช้ปลายทาง
---
## Permission Review
### Employee
Can View:
- ไม่ควรเข้าหน้า `Pending Review`
- ไม่ควรเข้าหน้า review รายการ
- ไม่ควรเรียก review API
Can Edit:
- ไม่มีสิทธิ์
Can Delete:
- ไม่มีสิทธิ์
หลักฐาน:
- หน้า `Pending Review` ใช้ `requireHRDDashboardAccess()`
- review API เช็ก `canReviewTrainingRecords(membership.role)`
### HRD
Can View:
- เข้าหน้า `Pending Review`
- เข้าหน้า review รายการ
- ดูรายการใน queue ได้ทั้งองค์กร
Can Edit:
- อนุมัติ / ไม่อนุมัติรายการที่ยัง `pending`
Can Delete:
- ไม่มี delete action ในหน้านี้
### Admin IT / Super Admin
Can View:
- เข้าหน้า `Pending Review` ได้ผ่าน business role mapping
- เข้าหน้า review และ review API ได้ ถ้ามี organization membership ในบริบทนั้น
Can Edit:
- อนุมัติ / ไม่อนุมัติได้เหมือน HRD
Can Delete:
- ไม่มี delete action ในหน้านี้
ข้อสังเกต:
- สิทธิ์ review จริงที่ API อิงกับ `membership.role === owner/admin` ไม่ได้อิงชื่อ business role โดยตรง
- ในทางปฏิบัติถือว่า HRD และ IT ใช้งานได้ แต่ logic สิทธิ์ยังเป็น “ตีความร่วม” มากกว่า matrix ที่ชัดเจน
---
## Functional Review
Checklist
| Feature | Status | Notes |
| ------- | ------ | ----- |
| Display pending records | Partial | หน้า default เป็น `status=pending` แต่ผู้ใช้ยังเปลี่ยนไปดู status อื่นได้ |
| Pagination | Pass | ใช้ shared `useDataTable` + `DataTablePagination` |
| Search | Pass | ใช้ text filter ที่ column `search` |
| Sorting | Pass | รองรับ server-side sorting |
| Filtering | Pass | มี filter ตาม `status`, `company`, `department`, `year` |
| Responsive layout | Pass | table shell รองรับ overflow และปุ่มหลักไม่ล้นง่าย |
| Loading state | Pass | มี `loading.tsx` |
| Empty state | Partial | มี empty state แต่ไม่มี reset filter / toolbar ใน state นี้ |
| Error state | Partial | มี `error.tsx` แต่แสดง raw error message |
| Refresh after action | Pass | mutation invalidate query + push + refresh |
| Correct badge/status | Partial | label ถูก แต่ visual distinction ยังไม่ชัด |
| Correct action buttons | Pass | มี `ตรวจสอบ` และ `ดูรายละเอียด` |
| Correct permissions | Pass | page/API กันสิทธิ์ค่อนข้างถูกต้อง |
---
## Code Quality Review
จุดที่ดี:
- แยก route, listing, table, columns, action menu, schema, service, queries, mutations, server data helper ชัดเจน
- ใช้ shared DataTable stack ตาม guideline
- ใช้ Zod validation ใน review API
- ใช้ React Query cache invalidation ตาม feature key
- ไม่มีการ import mock data
จุดที่ควรสังเกต:
- หน้า `Pending Review` reuse generic `training-records` list แทนที่จะมี query helper ที่สื่อชัดว่าเป็น review queue
- `TrainingRecordReviewDialog` ยังอยู่ใน feature แต่ไม่ได้เป็น flow หลักของหน้า review ปัจจุบัน ดูเหมือนมีโอกาสเป็น dead/legacy path
- การใช้ generic endpoint ทำให้ semantics ของหน้า review queue พึ่งพา URL filter มากเกินไป
---
## Security Review
จุดที่ดี:
- หน้า `Pending Review` กัน direct URL ด้วย `requireHRDDashboardAccess()`
- review page กัน direct URL ด้วย `requireHRDDashboardAccess()`
- review API ใช้ server-side auth ผ่าน `requireOrganizationAccess()`
- review API เช็กสิทธิ์ซ้ำผ่าน `canReviewTrainingRecords()`
- review API ป้องกันการ review ซ้ำโดยเช็กว่า record ต้องอยู่สถานะ `pending`
- data list ถูก scope ด้วย organization และ membership role
ความเสี่ยง:
- `error.tsx` แสดง `error.message` ตรง ๆ ซึ่งอาจเผยรายละเอียดภายใน เช่นข้อความจาก backend หรือฐานข้อมูล
---
## Performance Review
จุดที่ดี:
- ใช้ server prefetch + hydration
- ใช้ manual pagination / filtering / sorting ที่สอดคล้องกับ server-side data source
- query invalidation หลัง review ทำครบทั้ง list และ detail key
- ไม่พบ N+1 query ที่แย่ในระดับ row-by-row สำหรับตารางนี้
จุดที่ควรปรับ:
- หน้า `pending-review` ใช้ generic list query ที่โหลด certificate preview map สำหรับทุก row แม้ตารางนี้ไม่ได้แสดง certificate preview โดยตรง
- ถ้าหน้านี้มีการใช้งานบ่อยและข้อมูลเยอะ อาจแยก endpoint/query ของ review queue โดยตัด field ที่ไม่จำเป็นออกได้
---
## Problems Found
### High
- Description: หน้า `Pending Review` ไม่ได้บังคับให้แสดงเฉพาะรายการ `pending` จริง
- File: [page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/pending-review/page.tsx:24), [pending-review-table.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-table.tsx:35), [pending-review-columns.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-columns.tsx:117)
- Component: `Page`, `PendingReviewTable`, `pendingReviewColumns`
- Reason: หน้า redirect แค่กรณีไม่มี `status` แต่ยังเปิดให้ filter เป็น `approved`, `rejected`, `needs_revision`
- Impact: ชื่อหน้าและความหมายของ queue ไม่ตรงกับพฤติกรรมจริง ทำให้ผู้ใช้สับสน และทำให้ขอบเขตของ review queue ไม่ชัด
### Medium
- Description: เมื่อกรองแล้วไม่เจอข้อมูล ระบบซ่อน toolbar ทั้งก้อน ทำให้ผู้ใช้ไม่มีปุ่มล้าง filter จากหน้าจอเดียว
- File: [pending-review-table.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-table.tsx:60)
- Component: `PendingReviewTable`
- Reason: empty state return ก่อน render `DataTableToolbar`
- Impact: UX ติดขัด โดยเฉพาะเมื่อ user search/filter พลาดแล้วไม่รู้จะ reset อย่างไร
- Description: error boundary แสดง raw `error.message` ให้ผู้ใช้
- File: [error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/pending-review/error.tsx:17)
- Component: `PendingReviewError`
- Reason: render ข้อความ error จาก runtime ตรง ๆ
- Impact: เสี่ยงเผยข้อความภายในระบบ และทำให้ UX ดู technical เกินไป
- Description: หน้า queue ใช้ endpoint generic ของ training records แทน query ที่สื่อเฉพาะ review queue
- File: [pending-review-listing.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-listing.tsx:22), [training-record-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/server/training-record-data.ts:245)
- Component: `PendingReviewListingPage`, `listTrainingRecordsForOrganization`
- Reason: reuse ได้ แต่ semantics ของ feature ไม่ชัด
- Impact: maintainability ลดลง และทำให้หน้า queue รับ status/filter เกินขอบเขตหน้าที่
### Low
- Description: badge status ใช้รูปแบบ visual เดียวกันทุกสถานะ
- File: [pending-review-columns.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-columns.tsx:107)
- Component: `pendingReviewColumns`
- Reason: ใช้ `Badge variant="outline"` ทุกค่า
- Impact: สแกนสถานะได้ช้ากว่าที่ควร
- Description: filter `year`, `company`, `department` ยังเป็น text-based ทั้งหมด
- File: [pending-review-columns.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-columns.tsx:45)
- Component: `pendingReviewColumns`
- Reason: ใช้งานได้ แต่ UX ไม่แข็งแรงเท่า select/faceted options
- Impact: ใช้งานได้แต่ความเร็วในการกรองต่ำลง โดยเฉพาะกับผู้ใช้ธุรการ
- Description: `TrainingRecordReviewDialog` ดูเป็น flow เก่าที่ไม่ได้เป็น entry point หลักของ pending review ปัจจุบัน
- File: [training-record-review-dialog.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/training-record-review-dialog.tsx:1)
- Component: `TrainingRecordReviewDialog`
- Reason: หน้าปัจจุบันพาไป review page เต็มแทน dialog
- Impact: เพิ่มภาระในการดูแลโค้ดและทำให้ feature surface ดูซ้ำซ้อน
---
## Recommendations
- บังคับให้หน้า `Pending Review` เป็น `pending-only` จริงทั้ง route, filter UI, และ query contract
- ใน empty state ควรยังแสดง toolbar หรืออย่างน้อยมีปุ่ม `ล้างตัวกรอง`
- เปลี่ยน error message สำหรับผู้ใช้ให้เป็นข้อความที่ปลอดภัยและอ่านง่าย แล้ว log รายละเอียดไว้ภายในแทน
- ถ้าหน้า review queue เป็นฟีเจอร์สำคัญ ควรแยก query/helper สำหรับ review queue โดยเฉพาะ เพื่อให้ field และ logic ชัดกว่าการ reuse list generic
- เพิ่ม visual distinction ของ status badge
- พิจารณาปรับ filter บางตัวเป็น faceted/select หากมี option data พร้อมใช้อยู่แล้ว
- ตรวจสอบว่า `TrainingRecordReviewDialog` ยังจำเป็นหรือไม่ และถ้าไม่ใช้จริงควรวางแผน cleanup ในรอบถัดไป
---
## Missing Features
- การบังคับ queue ให้เป็น `pending-only` แบบชัดเจน
- ปุ่ม reset filter ใน empty state
- visual emphasis ของแต่ละ status
- query contract เฉพาะสำหรับ review queue
---
## Final Score
Architecture:
8/10
UI:
7/10
UX:
6.5/10
Performance:
7.5/10
Security:
8/10
Maintainability:
7.5/10
Overall:
7.4/10

View File

@@ -0,0 +1,319 @@
# Full System Audit Summary
## Executive Summary
Across Phases 1-7, the system shows strong implementation momentum but is not yet ready for production release. The codebase builds successfully, core CRUD flows are largely functional, and server-side authorization is present in many sensitive paths. However, the combined review found material gaps in security hardening, permission consistency, legacy architecture migration, operational readiness, and feature completeness.
The most serious blockers are concentrated in a few areas: public exposure of online-lesson assets, unsafe spreadsheet import posture, missing automated release gates, incomplete backup and rollback readiness, and an authorization model that is not yet consistently enforced from one source of truth. In parallel, the architecture still carries unresolved drift between the intended `users`-centric model and legacy `employees` usage, while several major workflow files are too large and too coupled for low-risk iteration.
Overall recommendation: **No-Go** until Wave 0 and Wave 1 remediation items are complete.
## Overall System Health
| Dimension | Status | Notes |
|---|---|---|
| Build Health | Partially Healthy | `lint`, `tsc`, and `build` pass, but no `test` script or CI gate exists. |
| Functional Coverage | Partially Healthy | Core CRUD works, but reports, review workflow, and training-policy lifecycle are incomplete. |
| Authorization Model | At Risk | Guards exist, but enforcement is split between permission templates and legacy role helpers. |
| Security Posture | At Risk | Public lesson assets, unbounded spreadsheet imports, and unresolved dependency advisories remain. |
| UI/UX Consistency | At Risk | Encoding corruption and cross-feature inconsistency materially reduce production quality. |
| Maintainability | At Risk | Oversized route handlers, large client forms, and duplicated workflow logic raise regression risk. |
| Production Operations | Not Ready | No CI/CD workflow, no health endpoint, no verified restore/rollback process, stale docs. |
## Findings Summary by Severity
### High
- `F-001` Runtime architecture still depends heavily on legacy `employees` despite the documented `users`-first target model.
- `F-002` Training-record review workflow lacks a true rejected path.
- `F-003` Reports module is incomplete for expected report coverage and export formats.
- `F-004` Training-policy lifecycle is incomplete: delete/soft delete, effective dating, and versioning are missing.
- `F-005` Permission templates are not the true single source of truth; sensitive paths still rely on compatibility role helpers.
- `F-006` Thai text encoding corruption is widespread in production UI.
- `F-007` Several route handlers, server data modules, and client workflow forms are oversized and over-coupled.
- `F-008` Online-lesson uploaded assets are publicly accessible by direct URL.
- `F-009` Spreadsheet import endpoints accept unbounded `.xlsx` uploads and rely on a dependency with active high-severity advisories.
- `F-010` Production release discipline is incomplete: no automated tests, no CI pipeline, and no health/readiness endpoint.
### Medium
- `F-011` Production dashboard still exposes demo/template/legacy routes.
- `F-012` Page guards, nav guards, and API guards are inconsistent across modules.
- `F-013` Announcements and online lessons lack scheduling/versioning capabilities expected for publishing workflows.
- `F-014` Notifications are limited and incomplete as a delivery system.
- `F-015` Reports bypass shared table patterns and remain visually dense.
- `F-016` API/client/error handling and some data integrity rules are still generic or app-level only.
- `F-017` Rate limiting, CSRF/origin hardening, and explicit security headers are not implemented centrally.
- `F-018` Deployment and environment docs are stale and partly inherited from the starter template.
### Low
- `F-019` Proxy perimeter coverage is incomplete but partially compensated by route-handler guards.
- `F-020` Encoding corruption also appears in permission metadata and supporting UI text, reducing trust and maintainability.
## Findings Summary by Module
| Module | Key Findings |
|---|---|
| Auth / Session / Proxy | Permission model split, inconsistent perimeter, fallback secret posture, missing abuse protection. |
| Users / Employees | Architecture drift between intended `users` runtime model and legacy `employees` dependencies. |
| Training Records | Review workflow incomplete, large form boundary, import hardening needed. |
| Reports | Functional gaps, role-scoped authorization drift, duplicated scope resolution, coupled export/data logic, dense UX. |
| Training Policy | Missing lifecycle completeness and version/effective-date controls. |
| Training Matrix | Missing route target, inconsistent page/API guard placement. |
| Announcements | Large workflow handler, publishing lifecycle gaps, attachment UX differs from other modules. |
| Online Lessons | Public asset exposure, large client form, attachment workflow inconsistency, publishing capability gaps. |
| Permission Management | Template model not yet authoritative, metadata encoding issues, DB integrity TODO remains. |
| Overview / Dashboard | Large server aggregation modules, legacy/demo routes still present in production surface. |
| Platform / Ops | No CI/CD, no tests, no health endpoint, stale docs, incomplete runbooks. |
## Production Blockers
| Blocker | Problem | Impact | Module | Recommended Owner | Dependency | Acceptance Criteria |
|---|---|---|---|---|---|---|
| `PB-01` | Online-lesson files are served as public URLs from `public/uploads/...`. | Unauthorized data access and uncontrolled file distribution. | Online Lessons / Storage | Backend | Storage refactor | Lesson assets require authenticated, organization-aware download access. |
| `PB-02` | Spreadsheet import accepts unbounded `.xlsx` uploads and uses vulnerable parsing dependency. | DoS risk and unsafe untrusted file parsing. | Training Records / Employees Import | Backend | Dependency upgrade | File-size limits, MIME validation, rate limits, and upgraded parser are in place. |
| `PB-03` | Authorization decisions are not consistently permission-first across pages, nav, and APIs. | Privilege drift and inconsistent access behavior. | Auth / Reports / Matrix / Employees / Audit Logs | Backend | Permission model cleanup | Sensitive routes and pages resolve access from the same permission source of truth. |
| `PB-04` | No CI pipeline, no automated test suite, no release gate. | Regressions can reach production without enforcement. | Platform / Repo | DevOps / Full-stack | Test baseline | PR workflow runs lint, typecheck, build, and targeted tests before merge. |
| `PB-05` | No health/readiness endpoint and no documented rollback or restore procedure. | Slow incident detection and unsafe deployment recovery. | Platform / Deployment / DB Ops | DevOps | Operational docs | Health endpoint exists and restore/rollback runbooks are documented and verified. |
| `PB-06` | Docs and deployment artifacts still contain stale starter-template and Clerk-oriented assumptions. | Incorrect deployment setup and operator confusion. | Docs / Docker / README | Full-stack | Architecture alignment | README, env docs, Docker notes, and deployment instructions match current Auth.js TMS behavior. |
| `PB-07` | No automated tests or verified smoke coverage for critical business workflows. | Production behavior cannot be trusted after fixes or releases. | Whole System | Full-stack / QA | Test harness | Critical workflows have automated coverage and manual smoke checklist. |
## Cross-Phase Root Causes
- Incomplete migration from the original starter/template application into a focused TMS product.
- Incomplete migration from legacy `employees` ownership to the documented `users`-centric runtime model.
- Incomplete migration from compatibility role checks to permission-first authorization.
- Workflow-heavy features have grown inside large route handlers and large client components instead of being decomposed into smaller services.
- Shared UI primitives exist, but design-system governance is inconsistent across older and newer modules.
- Operational maturity lagged behind feature delivery: testing, CI/CD, monitoring depth, and runbooks were not finished alongside implementation.
## Risk Matrix
| Risk ID | Risk | Likelihood | Impact | Overall |
|---|---|---|---|---|
| `R-01` | Unauthorized access to online-lesson assets | High | High | Critical |
| `R-02` | Spreadsheet import abuse or parser exploitation | High | High | Critical |
| `R-03` | Permission drift exposes data incorrectly across reports and admin modules | Medium | High | High |
| `R-04` | Release regression reaches production without test or CI gate | High | High | Critical |
| `R-05` | Incident recovery fails due to absent backup/rollback process | Medium | High | High |
| `R-06` | Large coupled modules cause high-risk regressions during remediation | High | Medium | High |
| `R-07` | Users lose trust due to corrupted Thai content and inconsistent workflows | High | Medium | High |
| `R-08` | Architecture drift around `employees` vs `users` blocks future feature correctness | Medium | High | High |
## Traceability Matrix
| Finding ID | Phase | Severity | Module | Production Blocker | Recommended Wave | Related Finding |
|---|---|---|---|---|---|---|
| `F-001` | 1 | High | Users / Employees / Reports | No | Wave 2 | `F-003`, `F-012` |
| `F-002` | 2 | High | Training Records | No | Wave 1 | `F-007` |
| `F-003` | 2 | High | Reports | No | Wave 2 | `F-001`, `F-005`, `F-015` |
| `F-004` | 2 | High | Training Policy | No | Wave 2 | `F-007` |
| `F-005` | 3 | High | Auth / Permissions / Reports | Yes (`PB-03`) | Wave 1 | `F-012`, `F-019` |
| `F-006` | 4 | High | Shared UI / Content | No | Wave 1 | `F-020` |
| `F-007` | 5 | High | Announcements / Reports / Overview / Training Records | No | Wave 2 | `F-016` |
| `F-008` | 6 | High | Online Lessons / Storage | Yes (`PB-01`) | Wave 0 | `F-013`, `F-017` |
| `F-009` | 6 | High | Import Endpoints / Dependencies | Yes (`PB-02`) | Wave 0 | `F-017` |
| `F-010` | 7 | High | Repo / Platform / Deployment | Yes (`PB-04`, `PB-05`, `PB-07`) | Wave 1 | `F-018` |
| `F-011` | 1 | Medium | Dashboard Surface | No | Wave 1 | `F-018` |
| `F-012` | 1,3 | Medium | Page Guards / APIs / Nav | No | Wave 1 | `F-005`, `F-001` |
| `F-013` | 2 | Medium | Announcements / Online Lessons | No | Wave 3 | `F-008` |
| `F-014` | 2 | Medium | Notifications | No | Wave 3 | `F-003` |
| `F-015` | 4 | Medium | Reports UI | No | Wave 2 | `F-003` |
| `F-016` | 5 | Medium | API Client / Permission Data / Shared Utils | No | Wave 2 | `F-007` |
| `F-017` | 6 | Medium | Security Platform | No | Wave 1 | `F-008`, `F-009` |
| `F-018` | 7 | Medium | Docs / Env / Docker | Yes (`PB-06`) | Wave 1 | `F-011` |
| `F-019` | 1,6 | Low | Proxy / Perimeter | No | Wave 3 | `F-005` |
| `F-020` | 3,4 | Low | Permission Metadata / Shared UI Copy | No | Wave 1 | `F-006` |
## Remediation Roadmap
### Wave 0 - Emergency Fix
- Close public access to online-lesson assets and replace direct file URLs with guarded delivery.
- Harden spreadsheet import surfaces with file-size limits, MIME validation, rate limiting, dependency upgrades, and safer parsing controls.
- Re-run dependency audit and clear high-severity advisories affecting active runtime paths.
### Wave 1 - Pre-Production Fix
- Unify authorization around permission-first server checks for reports, matrix, employees, audit logs, and org-management paths.
- Remove, quarantine, or explicitly disable demo/template production routes that should not ship.
- Fix Thai text encoding corruption across navigation, forms, labels, pagination, and permission metadata.
- Add CI release gates: lint, typecheck, build, and critical test workflow.
- Add health/readiness endpoints plus rollback, restore, and deployment runbooks.
- Update README, env documentation, Docker instructions, and release notes to match current Auth.js TMS architecture.
### Wave 2 - Stabilization
- Refactor oversized route handlers, report data modules, overview aggregators, and workflow-heavy forms into smaller service boundaries.
- Complete missing business lifecycle behavior in training-policy and training-record review flows.
- Normalize reporting architecture: authorization scope resolution, export generation, and shared UI patterns.
- Add missing data integrity constraints, typed error models, and reusable validation utilities.
- Continue the `employees` to `users` migration in runtime-critical flows.
### Wave 3 - Continuous Improvement
- Add scheduling/versioning features to content publishing modules where required by the product.
- Mature notification delivery beyond current in-app limitations.
- Expand structured logging, correlation IDs, security headers, CSP, and abuse monitoring.
- Tighten perimeter protection and continue cleanup of legacy/demo/dead code.
## Coding Phase Backlog
### `AUDIT-001` Secure Online Lesson Asset Delivery
- Problem: lesson files are directly accessible without authenticated authorization checks.
- Scope: move online-lesson file access to guarded download/stream endpoints and stop serializing raw public URLs.
- Out of Scope: full cloud object-storage migration.
- Files / Modules: `src/lib/online-lesson-storage.ts`, `src/features/online-lessons/server/online-lesson-data.ts`, `src/features/online-lessons/components/online-lesson-detail-page.tsx`, related route handlers.
- Acceptance Criteria: unauthenticated direct URL access fails; authorized users can still view/download assets through guarded endpoints.
- Test Required: route authorization tests, manual cross-role smoke test, file access regression test.
- Dependency: `AUDIT-006`.
- Severity: High.
- Priority: P0.
### `AUDIT-002` Harden Spreadsheet Import Surface
- Problem: imports rely on extension-only validation, unbounded memory reads, and vulnerable parsing dependency.
- Scope: add file-size caps, MIME checks, rate limits, parser upgrade/replacement, and explicit failure telemetry.
- Out of Scope: redesign of import mapping UX.
- Files / Modules: `src/app/api/training-records/import/route.ts`, `src/app/api/import-employees/route.ts`, `package.json`, lockfile.
- Acceptance Criteria: oversized or invalid files are rejected early; dependency audit shows no high advisory on active import path.
- Test Required: upload validation tests, negative import smoke tests, dependency audit verification.
- Dependency: none.
- Severity: High.
- Priority: P0.
### `AUDIT-003` Unify Permission Enforcement
- Problem: permission templates and role compatibility helpers produce inconsistent access rules.
- Scope: define authoritative permission checks for pages, APIs, and nav visibility in sensitive modules.
- Out of Scope: complete product-wide RBAC redesign beyond current permission catalog.
- Files / Modules: `src/lib/auth/session.ts`, `src/lib/auth/roles.ts`, reports, training-matrix, employees, audit logs, organizer access paths.
- Acceptance Criteria: the same permission decision applies across nav, page load, and API action for each reviewed module.
- Test Required: authorization matrix tests by role/permission, manual direct-URL verification.
- Dependency: `AUDIT-008`.
- Severity: High.
- Priority: P0.
### `AUDIT-004` Establish Release Gate and Smoke Coverage
- Problem: repository has no CI workflow, no automated tests, and no production release gate.
- Scope: add CI workflow for lint, typecheck, build, and critical smoke/test jobs; add `npm test` or equivalent script.
- Out of Scope: full end-to-end suite for every feature.
- Files / Modules: `.github/workflows/*`, `package.json`, test harness folders.
- Acceptance Criteria: pull requests fail on broken lint/type/build/tests; critical user flows have executable automated checks.
- Test Required: CI self-verification on a test branch.
- Dependency: none.
- Severity: High.
- Priority: P0.
### `AUDIT-005` Add Health, Rollback, and Restore Readiness
- Problem: operational recovery and readiness signals are missing.
- Scope: add health/readiness endpoints and document rollback, backup, and restore procedures.
- Out of Scope: full SRE platform implementation.
- Files / Modules: `src/app/api/*`, `README.md`, ops docs, deployment docs.
- Acceptance Criteria: operators can verify service health and follow documented restore/rollback steps.
- Test Required: endpoint smoke tests, tabletop recovery walkthrough.
- Dependency: `AUDIT-004`.
- Severity: High.
- Priority: P0.
### `AUDIT-006` Remove Public File URL Assumptions
- Problem: file-serving patterns are inconsistent across modules, and online lessons diverge from guarded download patterns already used elsewhere.
- Scope: standardize protected file access conventions across announcements, certificates, and lesson assets.
- Out of Scope: media CDN optimization.
- Files / Modules: file storage helpers, download routes, serialization contracts.
- Acceptance Criteria: protected uploads are never exposed as unconditional public URLs.
- Test Required: file access regression tests.
- Dependency: none.
- Severity: Medium.
- Priority: P1.
### `AUDIT-007` Repair Thai Encoding and Shared UX Copy
- Problem: corrupted Thai text degrades usability and trust across the product.
- Scope: fix encoding/copy corruption in nav, forms, pagination, empty states, and permission metadata.
- Out of Scope: net-new content strategy.
- Files / Modules: shared UI copy, feature labels, permission metadata catalogs.
- Acceptance Criteria: reviewed screens display valid Thai copy consistently.
- Test Required: manual UI verification across major screens.
- Dependency: none.
- Severity: High.
- Priority: P1.
### `AUDIT-008` Refactor Oversized Workflow Modules
- Problem: several large route handlers and components hold too many responsibilities.
- Scope: split report data logic, announcement workflows, overview aggregation, and training-record form orchestration into smaller units.
- Out of Scope: unrelated feature redesign.
- Files / Modules: `src/app/api/announcements/[id]/route.ts`, `src/features/reports/server/report-data.ts`, `src/features/overview/server/overview-data.ts`, `src/features/training-records/components/training-record-form.tsx`.
- Acceptance Criteria: each target module has clearer single-responsibility boundaries and lower regression surface.
- Test Required: focused regression tests around refactored workflows.
- Dependency: `AUDIT-004`.
- Severity: High.
- Priority: P1.
### `AUDIT-009` Complete Missing Business Workflow Paths
- Problem: training-record rejection, training-policy lifecycle, and report coverage remain incomplete.
- Scope: implement rejected review path, policy lifecycle operations, and prioritized missing report outputs.
- Out of Scope: non-priority analytics expansion.
- Files / Modules: training-records, training-policy, reports.
- Acceptance Criteria: required workflow states and agreed report set are supported end-to-end.
- Test Required: workflow tests, export smoke tests, role-scoped manual verification.
- Dependency: `AUDIT-003`.
- Severity: High.
- Priority: P1.
### `AUDIT-010` Align Runtime Data Model Around `users`
- Problem: architecture documentation and runtime implementation still diverge on person ownership.
- Scope: identify remaining runtime-critical `employees` dependencies and migrate them toward `users`-based ownership where required.
- Out of Scope: historical legacy-data archival strategy.
- Files / Modules: auth/session joins, reports, overview, training-policy, training-record participant flows.
- Acceptance Criteria: runtime-critical training flows no longer require legacy `employees` as the effective source of truth.
- Test Required: data-scope regression tests and migration verification.
- Dependency: `AUDIT-003`, `AUDIT-009`.
- Severity: High.
- Priority: P2.
## Testing Strategy After Fix
- Run mandatory gates on every change set: `npm run lint`, `npx tsc --noEmit`, `npm run build`, and the new automated test command.
- Add authorization matrix tests covering nav/page/API behavior for `super_admin`, `admin`, and `user`.
- Add storage access tests for protected download and streaming routes.
- Add import validation tests for size, file type, malformed spreadsheet, and permission failure cases.
- Add regression tests for training-record review transitions and prioritized report exports.
- Run a manual production-like smoke checklist covering login, org switching, CRUD basics, imports, exports, file access, and audit logging.
## Recommended Release Gate
The release gate should require all of the following:
- Wave 0 completed and verified.
- Wave 1 completed for permission consistency, encoding repair, CI gate, health/readiness, and documentation alignment.
- No unresolved high-severity security advisory on active runtime dependencies.
- Critical-path automated coverage exists for auth, authorization, imports, exports, and protected file delivery.
- Backup, restore, and rollback procedures are documented and exercised at least once.
- Final UAT is re-run after remediation on a production-like environment.
## Final Go-live Recommendation
**No-Go**.
The system is close enough to justify continued investment, but not close enough to justify production release. The combination of security blockers, missing release discipline, authorization inconsistency, and operational readiness gaps creates too much avoidable risk for go-live.
No material contradiction was found across Phases 1-7. The findings reinforce one another: structural drift identified in the architecture and permission reviews directly explains later quality, security, and production-readiness issues.
## Appendix: Source Reports
- [Phase 1 - Architecture Audit](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/reviews/phase-1-architecture-audit.md)
- [Phase 2 - Functional Review](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/reviews/phase-2-functional-review.md)
- [Phase 3 - Permission & Authorization Audit](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/reviews/phase-3-permission-authorization-audit.md)
- [Phase 4 - UI/UX & Design System Audit](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/reviews/phase-4-ui-ux-design-system-audit.md)
- [Phase 5 - Code Quality & Performance Audit](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/reviews/phase-5-code-quality-performance-audit.md)
- [Phase 6 - Security Audit](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/reviews/phase-6-security-audit.md)
- [Phase 7 - Production Readiness Review](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/reviews/phase-7-production-readiness-review.md)

View File

@@ -0,0 +1,300 @@
# Phase 1 - Architecture Audit
## Executive Summary
ภาพรวมสถาปัตยกรรมของระบบปัจจุบันมีแกนหลักที่ชัดเจนในระดับ app-owned stack คือ `Next.js App Router + Auth.js + Drizzle + PostgreSQL + TanStack Query` และหลาย feature หลักเดินตาม pattern `page -> feature api/service -> route handler -> server data -> db` ได้ดี โดยเฉพาะ `training-records`, `courses`, `announcements`, `reports`, และ `permission-management`
อย่างไรก็ตาม implementation ปัจจุบันยังมี architectural drift อยู่หลายจุดเมื่อเทียบกับ target architecture ในเอกสารโครงการ โดยประเด็นสำคัญที่สุดคือ runtime ยังพึ่งพาโมเดล `employees` อย่างกว้างขวาง แม้เอกสารสถาปัตยกรรมจะระบุว่า `users` ควรเป็น runtime person entity หลักและ `employees` เป็น legacy migration data เท่านั้น นอกจากนี้ยังพบ production dashboard surface ที่ยังรวม route กลุ่ม demo/template/legacy และมีความไม่สม่ำเสมอของ guard placement และ route contract บางจุด
## System Shape Observed
- Framework: Next.js 16 App Router
- Auth boundary: `src/auth.ts`, `src/lib/auth/session.ts`, `src/lib/auth/page-guards.ts`, `src/proxy.ts`
- Data boundary: `src/app/api/*` route handlers + Drizzle schema ใน `src/db/schema.ts`
- Feature modularization: ส่วนใหญ่ใช้ `src/features/<feature>` ตามแนวทางที่เอกสารกำหนด
- Canonical app-owned domains ที่เห็นชัด: `users`, `organizations`, `memberships`, `courses`, `training_records`, `training_matrices`, `training_policies`, `announcements`, `online_lessons`, `reports`, `permissions`
## Strengths
- มีการแยก auth/session, page guard, API handler, และ feature server helpers ค่อนข้างชัด
- route handlers สำคัญส่วนมากไม่เข้าฐานข้อมูลจาก UI โดยตรง แต่ผ่าน service/server data helpers
- RBAC model แบบ `systemRole + membership.role + effective permissions` ถูกวางโครงไว้ครบทั้ง session และ permission-resolution
- feature หลักจำนวนมากเดินตาม shared stack เดียวกัน เช่น `PageContainer`, TanStack Query, shared table/form primitives
## Findings
### [High] Runtime architecture ยังผูกกับ legacy `employees` อย่างลึก แม้ target architecture ระบุให้ `users` เป็น runtime person entity หลัก
**Module:** Auth, Schema, Training Records, Reports, Overview, Employee Directory, Training Policy
**ตำแหน่งที่พบ:**
- `src/db/schema.ts` ประมาณบรรทัด 81, 281, 438, 541
- `src/auth.ts` ประมาณบรรทัด 14, 126, 155, 285
- `src/features/employees/server/employee-identity-linking.ts`
- `src/features/training-records/server/training-record-data.ts`
- `src/features/reports/server/report-data.ts`
- `src/features/overview/server/overview-data.ts`
- `src/features/training-policy/server/employee-training-target-data.ts`
**รายละเอียด:**
เอกสาร `docs/PROJECT_ARCHITECTURE.md` และ `AGENTS.md` ระบุว่า runtime person data ควรอยู่บน `users` และ `employees` เป็น legacy migration data เท่านั้น แต่ implementation ปัจจุบันยังใช้ `employees` เป็น dependency หลักในหลาย workflow สำคัญ เช่น:
- `users.employeeId` ยังอ้างอิง `employees.id`
- มีตาราง `user_employee_maps`
- `training_records.employeeId` ยังอ้างอิง `employees.id`
- `employee_training_targets.employeeId` ยังถูกใช้ใน target resolution
- session enrichment ใน `src/auth.ts` เรียก `syncUserEmployeeLink(...)` ตอน login
- reports, overview, training matrix compliance, และ training records query หลักยัง join ผ่าน `employees`
**ผลกระทบ:**
- target architecture กับ implementation ปัจจุบันไม่สอดคล้องกัน
- การพัฒนาฟีเจอร์ใหม่มีความเสี่ยงสร้าง dual-source-of-truth ระหว่าง `users` และ `employees`
- การวาง roadmap ย้ายข้อมูลหรือ permission scoping ต่อจากนี้จะซับซ้อนขึ้น
**หลักฐานจากโค้ด:**
- `src/auth.ts` เรียก `syncUserEmployeeLink(...)` ระหว่าง authorize และเก็บ `employeeId` ลง session
- `src/db/schema.ts` ยังเก็บ foreign key หลายจุดไปที่ `employees`
- `src/features/reports/server/report-data.ts` และ `src/features/overview/server/overview-data.ts` ใช้ `employees` เป็นฐานการคำนวณ scope และ dataset
**สถานการณ์ตัวอย่าง:**
หากทีมพัฒนาฟีเจอร์ใหม่โดยอิงเอกสารอย่างเดียว จะเข้าใจว่า `users` คือ runtime source of truth แต่เมื่อเชื่อมกับ reports หรือ training records จริง ระบบยังต้องพึ่ง `employees` อยู่ ทำให้เกิดการ implement ผิดชั้นข้อมูลได้ง่าย
**คำแนะนำ:**
ระบุสถานะสถาปัตยกรรมปัจจุบันให้ชัดว่าอยู่ในช่วง transition และจัดทำ migration roadmap ระดับสถาปัตยกรรมก่อน coding phase แยกให้ชัดว่า domain ใดใช้ `users` จริงแล้ว และ domain ใดยังผูกกับ `employees`
**ต้องแก้ก่อน Production หรือไม่:** ใช่
### [Medium] Dashboard production surface ยังรวม route กลุ่ม demo/template/legacy ไว้ใน `app/dashboard`
**Module:** Dashboard routing, legacy/template features
**ตำแหน่งที่พบ:**
- `src/app/dashboard/billing/page.tsx`
- `src/app/dashboard/chat/page.tsx`
- `src/app/dashboard/forms/page.tsx`
- `src/app/dashboard/forms/advanced/page.tsx`
- `src/app/dashboard/forms/basic/page.tsx`
- `src/app/dashboard/forms/multi-step/page.tsx`
- `src/app/dashboard/forms/sheet-form/page.tsx`
- `src/app/dashboard/react-query/page.tsx`
- `src/app/dashboard/kanban/page.tsx`
- `src/app/dashboard/product/page.tsx`
- `src/app/dashboard/workspaces/page.tsx`
- `src/app/dashboard/elements/icons/page.tsx`
- `src/app/dashboard/exclusive/page.tsx`
**รายละเอียด:**
เอกสารสถาปัตยกรรมระบุให้บางพื้นที่ถูกมองเป็น legacy/template เท่านั้น แต่ route เหล่านี้ยังอยู่ใต้ dashboard จริง ทำให้ production route tree ปัจจุบันปะปนระหว่าง business modules กับ sample/demo/template modules
**ผลกระทบ:**
- เพิ่มความคลุมเครือว่าหน้าใดเป็น production-supported feature
- ทำให้การ audit permission, navigation, QA scope, และ release readiness ซับซ้อนขึ้น
- เพิ่ม maintenance overhead เพราะ route เหล่านี้ต้องถูกคำนึงถึงใน middleware, session, layout, และ regression surface
**หลักฐานจากโค้ด:**
- `src/app/dashboard/chat/page.tsx` render `ChatViewPage` ตรง
- `src/app/dashboard/react-query/page.tsx` ใช้ `features/react-query-demo`
- `src/app/dashboard/billing/page.tsx` ระบุชัดว่าเป็น placeholder หลังถอด Clerk billing ออกแล้ว
**สถานการณ์ตัวอย่าง:**
เมื่อทีมทำ full-system regression หรือ permission audit จะต้องตัดสินใจเองว่าหน้าเหล่านี้อยู่ในขอบเขต production หรือไม่ เพราะ route ยังอยู่ใน tree เดียวกับโมดูลธุรกิจหลัก
**คำแนะนำ:**
กำหนด policy ระดับสถาปัตยกรรมให้ชัดว่าหน้า demo/template ต้องย้ายออก, ปิด route, หรือ mark เป็น non-production module อย่างเป็นระบบ
**ต้องแก้ก่อน Production หรือไม่:** ควรพิจารณา
### [Medium] การวาง authorization guard ไม่สม่ำเสมอระหว่าง page routes
**Module:** Dashboard route access control
**ตำแหน่งที่พบ:**
- `src/app/dashboard/training-matrix/page.tsx`
- `src/app/dashboard/organizers/page.tsx`
- `src/app/dashboard/overview/page.tsx`
- `src/app/dashboard/layout.tsx`
- `src/lib/auth/page-guards.ts`
- `src/proxy.ts`
**รายละเอียด:**
บางหน้าใช้ shared page guards ตามมาตรฐาน (`requireEmployeeDashboardAccess`, `requireHRDDashboardAccess`, `requirePermissionDashboardAccess`) แต่บางหน้าใช้ custom auth/redirect เอง และบางหน้าพึ่ง proxy หรือ parent layout แทน โดยไม่มี local page guard ตรงตัว
ตัวอย่าง:
- `src/app/dashboard/training-matrix/page.tsx` ไม่เรียก page guard เลย
- `src/app/dashboard/organizers/page.tsx` ใช้ `auth()` + `isHRD(...)` + `redirect(...)` เอง แทน shared page guard
- `src/app/dashboard/overview/page.tsx` ไม่มี guard ตรงตัว โดย guard อยู่ที่ `src/app/dashboard/overview/layout.tsx`
- `src/app/dashboard/layout.tsx` ไม่มี auth check ในตัวเอง และอาศัย `src/proxy.ts` เป็น outer boundary
**ผลกระทบ:**
- access policy อ่านยากและตรวจสอบยาก
- เพิ่มโอกาสให้ route ใหม่ถูกสร้างโดยลืมวาง guard ในตำแหน่งที่เหมาะสม
- ลดความสม่ำเสมอของ architecture ที่เอกสารพยายามกำหนดไว้
**หลักฐานจากโค้ด:**
- `src/app/dashboard/training-matrix/page.tsx` parse search params และ render listing ทันที
- `src/app/dashboard/organizers/page.tsx` ไม่ใช้ helper จาก `src/lib/auth/page-guards.ts`
**สถานการณ์ตัวอย่าง:**
หากมีการ refactor proxy หรือย้าย layout/sub-layout ในอนาคต หน้าเหล่านี้อาจเปลี่ยนพฤติกรรมการป้องกันโดยไม่ได้ตั้งใจ เพราะ guard placement ไม่ได้อยู่รูปแบบเดียวกัน
**คำแนะนำ:**
กำหนด architecture rule เดียวสำหรับ dashboard pages ว่าต้องใช้ shared page guard ใน route entrypoint ทุกครั้ง หรือบันทึกข้อยกเว้นเป็น explicit convention
**ต้องแก้ก่อน Production หรือไม่:** ควรพิจารณา
### [Medium] Route contract ของ Training Matrix ไม่สมบูรณ์: มี CTA ไปยัง route ที่ไม่มีอยู่จริง
**Module:** Training Matrix dashboard route
**ตำแหน่งที่พบ:**
- `src/app/dashboard/training-matrix/page.tsx` ประมาณบรรทัด 28
- ไม่พบ `src/app/dashboard/training-matrix/new/page.tsx`
**รายละเอียด:**
หน้า `training-matrix` สร้างปุ่ม `Add Rule` ที่ลิงก์ไป `/dashboard/training-matrix/new` แต่จากโครงสร้างไฟล์ปัจจุบันไม่พบ route ปลายทางดังกล่าว
**ผลกระทบ:**
- route map ของ feature ไม่ครบตามที่ UI สื่อสาร
- เพิ่มสัญญาณว่าระบบยังมี implementation gap ระหว่าง page shell กับ feature workflow จริง
**หลักฐานจากโค้ด:**
- `src/app/dashboard/training-matrix/page.tsx` มี `href='/dashboard/training-matrix/new'`
- ตรวจไม่พบไฟล์ `src/app/dashboard/training-matrix/new/page.tsx`
**สถานการณ์ตัวอย่าง:**
ผู้ใช้ HRD/แอดมินกด `Add Rule` แล้วไปสู่ 404 หรือเส้นทางที่ไม่รองรับ ทำให้ workflow หลักของ matrix management ไม่ต่อเนื่อง
**คำแนะนำ:**
ระบุให้ชัดว่า feature นี้ตั้งใจใช้ modal/sheet แทน route create หรือมี route create จริงแต่ยังขาด implementation จากนั้นปรับ route contract ให้สอดคล้อง
**ต้องแก้ก่อน Production หรือไม่:** ใช่
### [Low] API perimeter protection ใน `proxy.ts` ใช้ allowlist แบบบางส่วน ทำให้ boundary strategy ไม่สม่ำเสมอ
**Module:** Middleware / API protection strategy
**ตำแหน่งที่พบ:**
- `src/proxy.ts`
- เปรียบเทียบกับ API modules ใต้ `src/app/api`
**รายละเอียด:**
`src/proxy.ts` ป้องกัน dashboard ทั้งหมด และ API บาง prefix เท่านั้น เช่น `/api/products`, `/api/training-records`, `/api/reports`, `/api/announcements` แต่ API กลุ่มใหม่บางส่วน เช่น `permission-templates`, `permission-audit-logs`, `user-permissions`, `permissions/catalog` ไม่ได้อยู่ใน `protectedApiPrefixes` และพึ่ง handler-level auth เป็นหลัก
ประเด็นนี้ไม่ใช่ auth bypass โดยตรง เพราะ handler หลายตัวมี `requireSuperAdminOrganizationAccess()` อยู่แล้ว แต่เป็นความไม่สม่ำเสมอของ perimeter architecture
**ผลกระทบ:**
- อ่าน boundary policy ได้ยาก
- เพิ่มความเสี่ยงที่ API ใหม่ในอนาคตจะถูกลืมเพิ่มเข้า proxy allowlist หรือทำ auth ซ้ำซ้อนแบบไม่เป็นระบบ
**หลักฐานจากโค้ด:**
- `src/proxy.ts` ระบุ `protectedApiPrefixes` แบบ hard-coded
- API permission management อยู่ใต้ `src/app/api/permission-templates`, `src/app/api/user-permissions`, `src/app/api/permission-audit-logs`, `src/app/api/permissions`
**สถานการณ์ตัวอย่าง:**
ทีมเพิ่ม API ใหม่ตาม feature module แล้วคิดว่า proxy คุ้มครองทุก API อยู่แล้ว ทั้งที่จริง route บางกลุ่มต้องพึ่ง handler guard เพียงอย่างเดียว
**คำแนะนำ:**
กำหนด perimeter strategy ให้เป็นแบบเดียว เช่น ปกป้อง API app-owned ทั้งหมดด้วย proxy matcher หรือประกาศชัดเจนว่า proxy มีไว้กัน unauthenticated only และ authorization ทั้งหมดอยู่ที่ handler
**ต้องแก้ก่อน Production หรือไม่:** ไม่ใช่
## Commands Executed and Results
1. `Get-Content -Raw docs/AI_DEVELOPMENT_GUIDE.md`
Result: ใช้เป็นเกณฑ์อ้างอิง pattern หลักของโปรเจ็กต์
2. `Get-Content -Raw docs/PROJECT_ARCHITECTURE.md`
Result: ใช้ยืนยัน target architecture และ legacy areas
3. `Get-Content -Raw plans/tms-system-full-audit-prompts.md`
Result: ใช้กำหนดรูปแบบรายงานและข้อห้ามระหว่าง audit
4. `Get-ChildItem src/features | Select-Object -ExpandProperty Name`
Result: ใช้ inventory feature modules
5. `rg -n "require(Employee|HRD|IT|Organization|SuperAdmin).*" src/app/dashboard src/app/api`
Result: ใช้สำรวจ route/page guard และ API guard ที่ใช้อยู่จริง
6. `Get-ChildItem -Recurse -Depth 2 src/app/dashboard | Select-Object FullName`
Result: ใช้ตรวจ route surface ทั้งหมดใน dashboard
7. `Get-Content -Raw src/auth.ts`
Result: พบ session enrichment และ employee-link sync
8. `Get-Content -Raw src/lib/auth/session.ts`
Result: ใช้ยืนยัน org access และ permission enforcement helper
9. `Get-Content -Raw src/lib/auth/page-guards.ts`
Result: ใช้เทียบกับ guard placement ในแต่ละ dashboard page
10. `Get-Content -Raw src/proxy.ts`
Result: ใช้ตรวจ perimeter protection ของ dashboard และ API
11. `rg -n "/dashboard/training-matrix/new" src`
Result: พบ CTA route reference เพียงจุดเดียวใน `training-matrix/page.tsx`
12. `Test-Path src/app/dashboard/training-matrix/new/page.tsx`
Result: `False`
## Files Reviewed
- `docs/AI_DEVELOPMENT_GUIDE.md`
- `docs/PROJECT_ARCHITECTURE.md`
- `plans/tms-system-full-audit-prompts.md`
- `src/auth.ts`
- `src/proxy.ts`
- `src/lib/auth/session.ts`
- `src/lib/auth/page-guards.ts`
- `src/lib/auth/roles.ts`
- `src/db/schema.ts`
- `src/app/dashboard/layout.tsx`
- `src/app/dashboard/page.tsx`
- `src/app/dashboard/overview/page.tsx`
- `src/app/dashboard/organizers/page.tsx`
- `src/app/dashboard/training-matrix/page.tsx`
- `src/app/dashboard/training-policy/page.tsx`
- `src/app/dashboard/users/page.tsx`
- `src/app/dashboard/billing/page.tsx`
- `src/app/dashboard/chat/page.tsx`
- `src/app/dashboard/react-query/page.tsx`
- `src/app/dashboard/product/page.tsx`
- `src/app/api/training-matrix/route.ts`
- `src/app/api/reports/export/route.ts`
- `src/app/api/auth/register/route.ts`
- `src/features/reports/server/report-data.ts`
- `src/features/products/components/product-listing.tsx`
- `src/features/organizers/components/organizer-listing.tsx`
- `src/features/chat/components/chat-view-page.tsx`
- `src/features/react-query-demo/components/pokemon-info.tsx`
- `src/features/employees/server/employee-identity-linking.ts`
## Areas Not Verified
- ไม่ได้รันแอปจริงใน browser เพื่อยืนยันพฤติกรรม redirect/404/runtime navigation
- ไม่ได้เชื่อมต่อฐานข้อมูลจริงเพื่อตรวจสอบ data migration state ระหว่าง `users` และ `employees`
- ไม่ได้ตรวจ coverage ของทุก route handler แบบครบทั้งระบบในเฟสนี้
- ยังไม่ได้ประเมินคุณภาพของ UI, performance, security hardening, หรือ production ops detail เพราะอยู่นอกขอบเขตของ Phase 1
## Recommended Next Step
Phase ถัดไปควรเป็น `Phase 2 - Functional Review` โดยเริ่มจาก feature inventory ของโมดูลธุรกิจหลักและแยกขอบเขต production module ออกจาก demo/template module ให้ชัด เพื่อให้ findings เชิง functional และ permission ใน phase ถัดไปไม่ปะปนกัน

View File

@@ -0,0 +1,499 @@
# Phase 2 - Functional Review
## Executive Summary
จากการตรวจ functional flow ของระบบ TMS ในรอบนี้ พบว่าฟีเจอร์หลักหลายส่วนสามารถใช้งานได้จริงในระดับ CRUD และ workflow พื้นฐาน เช่น login/session, employee directory, training records creation/import, announcements, online lessons, notifications แบบ in-app, และ training policy activation/history
อย่างไรก็ตาม เมื่อเทียบกับขอบเขตที่ระบุใน prompt แล้ว ระบบยังมี functional gap สำคัญหลายจุด โดยเฉพาะ:
- workflow รีวิว `training-records` ยังไม่รองรับ `Reject` แบบแยกจาก `Return for Edit`
- reports ที่รองรับจริงยังน้อยกว่าที่ต้องการ และไม่มี CSV export
- `training-policy` ยังไม่รองรับ delete/soft delete/effective date/versioning
- `announcements` และ `online-lessons` ยังไม่มี scheduled publish / versioning และบาง requirement ยังไม่ถูก model ไว้
- notifications ยังเป็น in-app only และ filter capability ไม่ครบกับ type ที่ระบบประกาศไว้
## Feature Inventory
| Module | Status | Notes |
|---|---|---|
| Authentication | Partial | Login/session unauthorized flow มี, self-service sign-up ถูกปิด, ไม่พบ first-time access flow หรือ LDAP/AD integration |
| Dashboard Overview | Partial | มี dashboard แยก admin/employee พร้อม year/company/department filters บาง role |
| Employee Management | Supported | มี list/search/filter/sort/pagination/detail/create/edit/import/targets |
| Training Records | Partial | Create/edit draft/submit/delete/import/certificate/download มี, review flow ยังไม่ครบ reject path |
| Online Lessons | Partial | Draft/submit/edit/review/publish/archive มี, schedule publish/versioning/completion ไม่พบ |
| Announcements | Partial | Draft/submit/edit/review/publish/archive/pin/unpin/expiration มี, audience/schedule publish/versioning ไม่พบ |
| Policy Management | Partial | List/create/edit/clone/activate/deactivate/history มี, delete/soft delete/effective date/versioning ไม่พบ |
| Approval Workflow | Partial | Content approval มีหลาย transition, training-record review ยังไม่ครบ state ตาม requirement |
| Permission Template Management | Partial | template CRUD-ish/clone/assign/overrides มี, soft delete และ default template behavior ต้องตรวจต่อใน Phase 3 |
| Reports | Partial | มี employee transcript/training matrix/department summary/annual summary + Excel/PDF export |
| Notifications | Partial | In-app/read-unread/link มี, email notification ไม่พบ |
| Audit Log | Supported | login/security event และ mutation modules หลักมี audit integration จำนวนมาก |
## Findings
### [High] Training Record review workflow ยังไม่รองรับ `Reject` แบบแยกจาก `Return for Edit`
**Module:** Training Records, Approval Workflow
**ตำแหน่งที่พบ:**
- `src/app/api/training-records/[id]/review/route.ts`
- `src/features/training-records/components/training-record-review-form.tsx`
- `src/features/training-records/api/types.ts`
- `src/features/training-records/schemas/training-record.ts`
**รายละเอียด:**
prompt ระบุให้ตรวจ flow `HRD Approve`, `HRD Reject`, `HRD Return for Edit` และ status path:
`Draft -> Pending Review -> Approved`
`Draft -> Pending Review -> Rejected`
`Draft -> Pending Review -> Returned for Edit -> Pending Review`
แต่ implementation ปัจจุบันของ training-record review รองรับเพียงสองผลลัพธ์:
- `approved`
- `needs_revision`
โดยไม่มี payload/action สำหรับ `rejected` แยกต่างหาก
**ผลกระทบ:**
- business workflow ของการรีวิวประวัติอบรมไม่ครบตาม requirement
- สถานะ `rejected` มีใน type/list/filter ของ training records แต่ไม่มีช่องทาง review flow ที่ทำให้ state นี้เกิดขึ้นในเส้นทางปกติ
- ผู้ใช้และผู้ตรวจอาจแยกไม่ออกระหว่าง “ส่งกลับให้แก้ไข” กับ “ปฏิเสธถาวร”
**หลักฐานจากโค้ด:**
- `src/features/training-records/api/types.ts` กำหนด `TrainingRecordReviewPayload.status` เป็น `'approved' | 'needs_revision'`
- `src/features/training-records/components/training-record-review-form.tsx` มี Tabs แค่ `approved` และ `needs_revision`
- `src/app/api/training-records/[id]/review/route.ts` map non-approved ทุกกรณีไป `approvalStatus: 'needs_revision'`
**สถานการณ์ตัวอย่าง:**
หาก HRD ต้อง “ปฏิเสธรายการอบรม” แบบจบ workflow โดยไม่เปิดให้แก้ไข ผู้ใช้จะทำไม่ได้ เพราะระบบรองรับแค่การอนุมัติหรือส่งกลับให้แก้ไข
**คำแนะนำ:**
แยก review decision ออกเป็น `approved` / `needs_revision` / `rejected` ให้ตรงกับ business rule และให้ type, schema, form, notification, audit log และ table filters สอดคล้องกัน
**ต้องแก้ก่อน Production หรือไม่:** ใช่
### [High] Reports module รองรับรายงานจริงไม่ครบตาม scope และไม่มี CSV export
**Module:** Reports
**ตำแหน่งที่พบ:**
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/api/types.ts`
- `src/app/api/reports/export/route.ts`
- `src/features/reports/server/report-data.ts`
**รายละเอียด:**
prompt ระบุให้ตรวจ:
- Employee Training Report
- Department Report
- Company Report
- K/S/A Report
- Required Hours Report
- Missing Training Report
- Approval Report
- Export CSV
- Export Excel
- Export PDF
- Filter
- Sort
- Pagination
แต่ implementation ปัจจุบันรองรับเพียง:
- Employee Transcript
- Training Matrix
- Department Summary
- Annual Training Summary
- Export Excel
- Export PDF
และไม่พบ:
- Company Report
- K/S/A report
- Required Hours report
- Missing Training report
- Approval report
- CSV export
- sorting/pagination ของ report output ในหน้า reports
**ผลกระทบ:**
- report module ยังไม่ครอบคลุม use case ที่ผู้ใช้งานสาย HRD/manager คาดหวัง
- งาน operational/reporting ต้องพึ่งการสรุปเองจากข้อมูลดิบหรือ export ไปทำต่อภายนอก
**หลักฐานจากโค้ด:**
- `src/app/api/reports/export/route.ts` รองรับ report แค่ `employeeTranscript`, `trainingMatrix`, `departmentSummary`, `annualTrainingSummary`
- `src/features/reports/components/reports-page-content.tsx` สร้าง export links เฉพาะ `xlsx` และ `pdf`
- ไม่พบโค้ดเกี่ยวกับ CSV export ใน `src/features/reports` และ `src/app/api/reports`
**สถานการณ์ตัวอย่าง:**
หาก HRD ต้องการรายงาน “พนักงานที่ยังขาดชั่วโมงบังคับ” หรือ “สรุปตามบริษัท” จะยังไม่สามารถดึงจาก report module ปัจจุบันได้โดยตรง
**คำแนะนำ:**
แยก report backlog ตาม report family และกำหนด output contract ที่ชัดเจน รวมถึงเพิ่ม CSV export และพิจารณา UX สำหรับ sort/pagination หรือ downloadable large-result patterns
**ต้องแก้ก่อน Production หรือไม่:** ใช่
### [High] Policy Management ยังไม่ครอบคลุม delete/soft delete/effective date/versioning ตาม requirement
**Module:** Policy Management
**ตำแหน่งที่พบ:**
- `src/app/api/training-policies/route.ts`
- `src/app/api/training-policies/[id]/route.ts`
- `src/app/api/training-policies/[id]/activate/route.ts`
- `src/app/api/training-policies/[id]/deactivate/route.ts`
- `src/app/api/training-policies/[id]/clone/route.ts`
- `src/app/api/training-policies/[id]/history/route.ts`
- `src/features/training-policy/api/types.ts`
**รายละเอียด:**
training policy ปัจจุบันรองรับ:
- list
- create
- edit
- activate/deactivate
- clone
- audit history
แต่ไม่พบ capability ที่ prompt ระบุไว้ ได้แก่:
- delete
- soft delete
- effective date
- versioning
data contract ของ policy ยังมีเพียง `year`, `total_hours`, `k_hours`, `s_hours`, `a_hours`, `is_active`
**ผลกระทบ:**
- การจัดการนโยบายย้อนหลังหรือเตรียม policy ล่วงหน้าแบบมีวันเริ่มใช้จริงทำไม่ได้
- การแยก “ร่าง version ถัดไป” กับ “policy ที่ใช้งานอยู่” ยังไม่ชัดเจนในเชิงโดเมน
**หลักฐานจากโค้ด:**
- `src/app/api/training-policies/route.ts` มีแค่ `GET` และ `POST`
- `src/app/api/training-policies/[id]/route.ts` มีแค่ `GET` และ `PATCH`
- `src/features/training-policy/api/types.ts` ไม่มี field กลุ่ม `effectiveDate`, `deletedAt`, `version`, `publishedAt`
- ไม่พบ `DELETE` route ใน `src/app/api/training-policies`
**สถานการณ์ตัวอย่าง:**
หากองค์กรต้องเตรียม policy ปีหน้าไว้ล่วงหน้าแต่ยังไม่ activate ทันทีตามวันมีผลบังคับใช้ ระบบยังไม่มี model/function ตรงนี้แบบ explicit
**คำแนะนำ:**
กำหนด business model ของ policy lifecycle ให้ชัดว่าใช้ปีอย่างเดียวพอหรือจำเป็นต้องมี effective date/version chain/retire state แล้วค่อยแตกเป็น API และ UX ที่ตรง requirement
**ต้องแก้ก่อน Production หรือไม่:** ใช่
### [Medium] Announcements และ Online Lessons ยังไม่มี scheduled publish / versioning และ requirement เสริมบางข้อยังไม่ถูก model
**Module:** Announcements, Online Lessons
**ตำแหน่งที่พบ:**
- `src/features/announcements/api/types.ts`
- `src/features/online-lessons/api/types.ts`
- `src/features/content-approval/lib/workflow.ts`
- `src/app/api/announcements/route.ts`
- `src/app/api/announcements/[id]/route.ts`
- `src/app/api/online-lessons/route.ts`
- `src/app/api/online-lessons/[id]/route.ts`
**รายละเอียด:**
ระบบ content workflow ปัจจุบันรองรับ state/action แบบทันที:
- draft
- pending_review
- approved
- published
- returned
- rejected
- archived
โดย action มีแค่ `submit/approve/return/reject/publish/archive`
สิ่งที่ prompt ต้องการแต่ยังไม่พบใน contract/model ปัจจุบัน:
- scheduled publish
- versioning
- audience targeting สำหรับ announcements
- lesson completion สำหรับ online lessons
announcement มี `startDate/endDate` ซึ่งช่วยเรื่องช่วงเวลาแสดงผลและ expiration ได้ แต่ไม่ใช่ scheduled publish แบบ explicit lifecycle
**ผลกระทบ:**
- content team ยังตั้งเวลาเผยแพร่ล่วงหน้าแบบ workflow จริงไม่ได้
- ไม่มี version chain สำหรับติดตามฉบับก่อน/หลัง
- announcement ยังไม่แยกกลุ่มผู้รับแบบ audience-specific
- online lesson ยังไม่ track completion state ของพนักงาน
**หลักฐานจากโค้ด:**
- `src/features/content-approval/lib/workflow.ts` ไม่มี action สำหรับ schedule/unpublish/version
- `src/features/announcements/api/types.ts` ไม่มี field audience/version/publishAt
- `src/features/online-lessons/api/types.ts` ไม่มี field completion/version/publishAt
**สถานการณ์ตัวอย่าง:**
หาก HRD ต้องเตรียมประกาศให้เผยแพร่วันจันทร์ 09:00 โดยอนุมัติล่วงหน้าวันศุกร์ หรืออยากเก็บ revision history ของบทเรียนออนไลน์ ระบบยังไม่รองรับ workflow นี้โดยตรง
**คำแนะนำ:**
แยก requirement ของ content lifecycle ออกเป็น:
- schedule model
- publication window
- audience scope
- version history
- completion tracking
แล้วทำให้ schema/API/UI ใช้ศัพท์เดียวกัน
**ต้องแก้ก่อน Production หรือไม่:** ควรพิจารณา
### [Medium] Notifications เป็น in-app only และ filter capability ไม่ครบกับ notification types ที่ระบบประกาศไว้
**Module:** Notifications
**ตำแหน่งที่พบ:**
- `src/features/notifications/api/types.ts`
- `src/app/api/notifications/route.ts`
- `src/features/notifications/server/notification-data.ts`
- `src/features/notifications/server/notification-service.ts`
**รายละเอียด:**
prompt ระบุให้ตรวจทั้ง `In-app Notification` และ `Email Notification`
implementation ปัจจุบันรองรับ:
- in-app notifications
- read/unread
- mark all read
- link navigation
แต่ไม่พบ email delivery implementation เช่น SMTP/provider/send function
อีกทั้ง type layer ประกาศ notification types ไว้กว้างกว่า filter API ที่รองรับจริง เช่น:
- `CONTENT_SUBMITTED`
- `CONTENT_APPROVED`
- `CONTENT_RETURNED`
- `CONTENT_REJECTED`
- `CONTENT_PUBLISHED`
มีใน `src/features/notifications/api/types.ts` และถูกสร้างได้จาก `notification-service.ts` แต่ `src/app/api/notifications/route.ts` เปิดให้ filter ได้เพียง subset หนึ่งเท่านั้น
**ผลกระทบ:**
- ผู้ใช้ที่คาดหวังการแจ้งเตือนทางอีเมลจะยังไม่ได้รับ
- filtering experience ของ notifications ไม่สอดคล้องกับ event types ที่ระบบเองประกาศไว้
**หลักฐานจากโค้ด:**
- `src/app/api/notifications/route.ts` ไม่มี logic ส่งอีเมล และ enum filter รองรับ subset จำกัด
- `src/features/notifications/server/notification-service.ts` มีแต่ `createNotification(...)` ลงตาราง notifications
- ไม่พบ library/provider สำหรับ email sending ใน codebase ที่ตรวจ
**สถานการณ์ตัวอย่าง:**
ผู้จัดการที่ไม่เข้า dashboard เป็นประจำจะไม่ถูกแจ้งผ่าน email เมื่อมี content รออนุมัติหรือประกาศใหม่ แม้ระบบจะมี event notification ระดับ in-app อยู่แล้ว
**คำแนะนำ:**
แยก notification channel model ให้ชัดว่าอะไรคือ in-app only และอะไรต้องมี email จากนั้นทำให้ notification type/filter API ครอบคลุม types ที่ระบบสร้างจริง
**ต้องแก้ก่อน Production หรือไม่:** ควรพิจารณา
### [Medium] Training Records ยังไม่พบ export feature ระดับโมดูล แม้ import/template/download certificate จะรองรับแล้ว
**Module:** Training Records
**ตำแหน่งที่พบ:**
- `src/app/dashboard/training-records/page.tsx`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/import/route.ts`
- `src/app/api/training-records/import/template/route.ts`
- `src/features/training-records/components/training-record-import-page.tsx`
**รายละเอียด:**
training records ปัจจุบันรองรับ:
- create/edit draft
- submit
- delete editable record
- import
- import template download
- certificate upload/download/delete
แต่ไม่พบ export flow ของ training records module โดยตรง แม้ prompt ระบุให้ตรวจ `Export`
**ผลกระทบ:**
- ผู้ใช้ต้องพึ่ง reports module หรือดึงข้อมูลจากตารางแทน หากต้องการ export รายการประวัติอบรมโดยตรงจากหน้า feature
**หลักฐานจากโค้ด:**
- ไม่พบ route กลุ่ม `/api/training-records/export`
- `src/app/dashboard/training-records/page.tsx` มี CTA สำหรับ import/template/review/create แต่ไม่มี export action
**สถานการณ์ตัวอย่าง:**
HRD ต้องการ export เฉพาะรายการจากหน้า training records หลัง filter แล้ว แต่ workflow ปัจจุบันไม่รองรับตรงตัว
**คำแนะนำ:**
ตัดสินใจให้ชัดว่า export ถือเป็น responsibility ของ report module เท่านั้น หรือควรมี export จาก training-record listing โดยตรง
**ต้องแก้ก่อน Production หรือไม่:** ควรพิจารณา
### [Low] Notification filter API กับ UI ไม่สอดคล้องกับ types ที่ระบบสร้างได้จริง
**Module:** Notifications
**ตำแหน่งที่พบ:**
- `src/features/notifications/api/types.ts`
- `src/app/api/notifications/route.ts`
- `src/features/notifications/constants/notification-options.ts`
- `src/features/notifications/server/notification-service.ts`
**รายละเอียด:**
ระบบสร้าง content workflow notifications ได้หลายประเภท แต่ GET `/api/notifications` filter schema รองรับไม่ครบ และหน้า notifications อิงจาก option set ที่แคบกว่า type universe จริง
**ผลกระทบ:**
- ผู้ใช้ filter หา event บางชนิดไม่ได้ ทั้งที่ระบบบันทึก notification ชนิดนั้นอยู่
**หลักฐานจากโค้ด:**
- `notification-service.ts` รองรับ `CONTENT_SUBMITTED`, `CONTENT_APPROVED`, `CONTENT_RETURNED`, `CONTENT_REJECTED`, `CONTENT_PUBLISHED`
- `src/app/api/notifications/route.ts` enum filter ไม่รวม type เหล่านี้ทั้งหมด
**สถานการณ์ตัวอย่าง:**
ทีมต้องการเปิดเฉพาะ “content returned” notifications เพื่อติดตามงานแก้ไข แต่หน้า UI/API ปัจจุบันยังไม่มี filter ตรงประเภทนี้
**คำแนะนำ:**
ทำ notification type registry กลาง และให้ API/UI/share same enum
**ต้องแก้ก่อน Production หรือไม่:** ไม่ใช่
## Positive Coverage Observed
- `employee-directory` รองรับ search/filter/sort/pagination/detail/create/import ได้ค่อนข้างครบ
- `announcements` รองรับ draft/submit/edit/review/publish/pin-unpin/expiration/employee view ได้ดีในระดับพื้นฐาน
- `online-lessons` รองรับทั้ง video URL และ video upload พร้อม thumbnail/attachment และ approval/publish flow พื้นฐาน
- `training-records` รองรับ draft/edit/delete/import/template/certificate handling ได้ค่อนข้างครบ
- overview และ reports มี role-aware scoping สำหรับ employee vs admin
## Commands Executed and Results
1. `rg -n "Phase 2|Functional Review|phase-2-functional-review" plans/tms-system-full-audit-prompts.md`
Result: ใช้ยืนยันหัวข้อและ output file ของ Phase 2
2. `Get-ChildItem -Recurse -File src/app/dashboard | Select-Object FullName`
Result: ใช้ inventory route/pages ของ dashboard
3. `Get-ChildItem -Recurse -File src/app/api | Select-Object FullName`
Result: ใช้ inventory route handlers ที่รองรับแต่ละ feature
4. `rg --files -g "*test*" -g "*spec*" src`
Result: ไม่พบ test coverage ที่มีนัยสำคัญสำหรับ functional verification
5. `rg -n "/dashboard/.*/new" src`
Result: ใช้เทียบ CTA create routes กับ implementation จริง
6. `rg -n "csv|CSV|text/csv|createCsv|export csv" src/features/reports src/app/api/reports`
Result: ไม่พบ CSV export ใน reports module
7. `rg -n "sendMail|nodemailer|resend|smtp|email" src/features src/app/api src/lib`
Result: ไม่พบ email notification implementation ที่ชัดเจน
8. `rg -n "delete|DELETE" src/app/api/training-policies src/features/training-policy`
Result: ไม่พบ delete route ของ training policies
9. `rg -n "effective|version|audience|schedule|publishAt|publishedAt|expires|expiration" ...`
Result: พบเพียง publish timestamp/expiration บางส่วน แต่ไม่พบ scheduled publish/versioning/audience model
## Files Reviewed
- `plans/tms-system-full-audit-prompts.md`
- `src/app/dashboard/training-records/page.tsx`
- `src/app/dashboard/training-records/[trainingRecordId]/page.tsx`
- `src/app/dashboard/announcements/page.tsx`
- `src/app/dashboard/announcements/[id]/page.tsx`
- `src/app/dashboard/online-lessons/page.tsx`
- `src/app/dashboard/online-lessons/manage/[id]/page.tsx`
- `src/app/dashboard/reports/page.tsx`
- `src/app/dashboard/notifications/page.tsx`
- `src/app/dashboard/employee-directory/page.tsx`
- `src/app/dashboard/overview/@area_stats/page.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-review-form.tsx`
- `src/features/training-records/components/training-record-view-page.tsx`
- `src/features/training-records/components/training-record-import-page.tsx`
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/announcements/components/announcement-view-page.tsx`
- `src/features/announcements/components/announcements-page.tsx`
- `src/features/online-lessons/components/online-lesson-form.tsx`
- `src/features/online-lessons/components/online-lesson-detail-page.tsx`
- `src/features/online-lessons/components/online-lessons-manage-page.tsx`
- `src/features/training-policy/components/training-policy-page.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/notifications/components/notifications-page.tsx`
- `src/features/notifications/server/notification-data.ts`
- `src/features/notifications/server/notification-service.ts`
- `src/features/content-approval/lib/workflow.ts`
- `src/features/employee-directory/components/employee-directory-columns.tsx`
- `src/features/employee-directory/components/employee-directory-header.tsx`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/api/training-records/import/route.ts`
- `src/app/api/training-records/import/template/route.ts`
- `src/app/api/announcements/route.ts`
- `src/app/api/announcements/[id]/route.ts`
- `src/app/api/online-lessons/route.ts`
- `src/app/api/online-lessons/[id]/route.ts`
- `src/app/api/training-policies/route.ts`
- `src/app/api/training-policies/[id]/route.ts`
- `src/app/api/reports/export/route.ts`
- `src/app/api/notifications/route.ts`
## Areas Not Verified
- ไม่ได้รัน UI จริงใน browser เพื่อยืนยัน end-to-end interaction, toast, redirect, และ empty/error state runtime behavior
- ไม่ได้ส่ง request จริงไปทุก API route พร้อมฐานข้อมูลจริง
- ไม่ได้ทดสอบไฟล์ upload/download จริง
- ไม่ได้ยืนยัน behavior ของ permission templates เชิงลึก เพราะ Phase 3 จะลงรายละเอียดกว่า
- ไม่ได้ยืนยัน notification delivery ภายนอกระบบ เพราะไม่พบ implementation email channel ในรอบนี้
## Recommended Next Step
Phase ถัดไปควรเป็น `Phase 3 - Permission & Authorization Audit` โดยเน้น:
- data scoping ของ employee vs HRD vs IT vs super admin
- route/API consistency
- effective permissions vs nav visibility
- ownership rules ใน training records / announcements / online lessons

View File

@@ -0,0 +1,446 @@
# Phase 3 - Permission & Authorization Audit
## Executive Summary
The authorization model is partially migrated to a permission-first design, but the live implementation is still mixed across `effectivePermissions`, `membership.role`, `businessRole`, and `systemRole`. That split creates real consistency risk: several sensitive pages and API routes still authorize by role-derived compatibility helpers even though the project now has permission templates, per-user overrides, and `effectivePermissions`.
The most important gap is that permission templates are not yet the single source of truth for access decisions. In practice, revoking a capability from an HRD/admin-style user through templates or overrides does not guarantee the capability is removed everywhere, because multiple paths still trust role-based helpers such as `requireHRD()`, `requireIT()`, `canManageTrainingMatrix(membership.role)`, and `canViewAllReports(role)`.
## Authorization Architecture Overview
Current authorization layers:
1. Authentication boundary
- `src/proxy.ts`
- Protects `/dashboard/**` and a subset of `/api/**` routes from unauthenticated access.
2. Session and organization access
- `src/lib/auth/session.ts`
- `requireSession()` -> `requireOrganizationAccess()` -> specialized guards.
3. Compatibility role helpers
- `src/lib/auth/roles.ts`
- Maps `super_admin`, membership roles, and legacy business roles into `IT`, `HRD`, and `EMPLOYEE`.
4. Permission helpers
- `src/lib/auth/authorization.ts`
- `hasPermission`, `hasAnyPermission`, `hasAllPermissions`.
5. Effective permission resolution
- `src/lib/auth/authorization.server.ts`
- Merges template assignments and user overrides into `effectivePermissions`.
6. Navigation filtering
- `src/config/nav-config.ts`
- `src/hooks/use-nav.ts`
- Mostly UX-only visibility filtering.
Observed architecture state:
- Permission-first exists and is used correctly in some modules such as `training-policy`.
- Compatibility-mode role checks still control many sensitive routes and data scopes.
- Navigation, page guards, API guards, and data-scope rules are not fully aligned yet.
## Permission Inventory
Defined permission families from `src/lib/auth/permissions.ts`:
- Organization: `organization:manage`, `organization:switch`
- Permission templates: `permission_template:*`
- User permission overrides: `user_permission:*`
- Users: `users:*`, `users:manage`
- Employee directory: `employee_directory:*`, `employee_import:run`, `employee_master_review:approve`
- Courses and matrix: `courses:*`, `training_policy:*`, `training_matrix:*`
- Training records: self/all read-write plus review workflow permissions
- Announcements: full publish workflow permissions
- Online lessons: full publish workflow permissions
- Reports: `reports:self_read`, `reports:organization_read`, `reports:export`, `reports:export_excel`, `reports:export_pdf`
- Audit logs: `audit_logs:read`, `audit_logs:export`
- Notifications: `notifications:read`, `notifications:manage`
Inventory assessment:
- Coverage is broad enough for a permission-first model.
- The problem is not missing permission keys; the problem is inconsistent enforcement of those keys.
## Route Permission Matrix
Representative page-level checks:
| Route | Nav Rule | Page Guard | Effective Result |
|---|---|---|---|
| `/dashboard/training-policy` | permission-based in nav | `requirePermissionDashboardAccess('training_policy:read')` | Consistent permission-first |
| `/dashboard/users` | `businessRoles: ['IT']` | `requireUserManagementDashboardAccess()` | Role-based compatibility check |
| `/dashboard/employee-directory` | `businessRoles: ['HRD', 'IT']` | `requireHRDDashboardAccess()` | Page is narrower than nav and ignores permission keys |
| `/dashboard/training-matrix` | `businessRoles: ['HRD', 'IT']` | no page guard | Direct URL can render page shell without authorization gate |
| `/dashboard/reports` | `businessRole: 'HRD'` | `requireEmployeeDashboardAccess()` | Page allows broader access than nav; server content later scopes by role |
| `/dashboard/audit-logs` | `systemRole: 'super_admin'` | `requireITDashboardAccess()` | Nav and page criteria differ |
| `/dashboard/organizers` | `systemRole: 'super_admin'` | inline `isHRD(...)` check | Page allows HRD-compatible access even though nav is super-admin only |
| `/dashboard/permissions/*` | `systemRole: 'super_admin'` | `requireSuperAdminDashboardAccess()` | Consistent, super-admin only |
## API Permission Matrix
Representative API-level checks:
| API Route | Guard Style | Assessment |
|---|---|---|
| `/api/training-policies` | `requirePermission('training_policy:read/write')` | Good permission-first enforcement |
| `/api/permission-templates` | `requireSuperAdminOrganizationAccess()` | Strong but role-based, not template-based |
| `/api/users` | `requireUserManagementAccess()` | Compatibility-mode `IT` check, not `users:*` permissions |
| `/api/training-matrix` | `requireOrganizationAccess()` + `canManageTrainingMatrix(membership.role)` | Role-based, bypasses `training_matrix:*` permissions |
| `/api/reports/export` | `getReportsAccess()` + `canViewAllReports(role)` | Scope based on membership role, not report permissions |
| `/api/training-records/[id]/review` | `requireOrganizationAccess()` + `canReviewTrainingRecords(membership.role)` | Role-based workflow approval |
| `/api/master-review` | `requireHRD()` | Role-based, bypasses employee master-review permission keys |
| `/api/employees` | `requireOrganizationAccess()` only | Any org member can list/create employees by direct API call |
| `/api/products` | `requireOrganizationAccess()` only | Any org member can list/create products by direct API call |
| `/api/notifications` | `requireOrganizationAccess()` + self-scope | Acceptable because data is always self-scoped |
## Action-Level Permission Matrix
Representative action enforcement:
| Action | UI Check | API Check | Assessment |
|---|---|---|---|
| View training policy | permission-based | permission-based | Consistent |
| Create/update training policy | permission-based | permission-based | Consistent |
| Review training record | mixed role/permission UI | role-based API | Template revokes may not be honored everywhere |
| Manage training matrix | nav role-based | role-based API | `training_matrix:write` is not the real gate |
| Export reports | page accessible broadly | export scope constrained by membership role | `reports:export*` keys do not drive the actual outcome |
| Manage employee directory | page role-based | employees API open to all org members | UI/API mismatch |
| Manage users | page/API `IT` compatibility | no `users:*` permission-first gate | Fine-grained permission model not fully active |
| Read audit logs | nav super-admin, page/API IT | inconsistent source of truth |
## Role / Template Mapping
Observed effective role mapping from `src/lib/auth/roles.ts`:
- `super_admin` -> treated as `IT`
- membership `owner`/`admin` -> treated as `HRD`
- membership `member` -> treated as `EMPLOYEE`
Template behavior from `src/lib/auth/authorization.server.ts`:
- If user has assigned permission templates, template permissions replace fallback role defaults.
- User overrides then allow or deny individual permissions.
Audit conclusion:
- The permission engine is capable of precise access control.
- The application layer still frequently ignores that engine and falls back to role compatibility rules.
- Role names and template names therefore do not reliably predict real access without tracing the exact route.
## Data Scope Assessment
Positive observations:
- Notifications are self-scoped by `organizationId + userId`.
- Training records use organization scoping and record-level lookup helpers.
- Reports distinguish self vs organization views in data-loading logic.
Scope weaknesses:
- Reports determine organization-wide visibility with `canViewAllReports(role)` in `src/features/reports/server/report-data.ts`, not with `reports:organization_read`.
- Department summary, annual summary, and training matrix reports are therefore governed by membership role instead of permission keys.
- Training matrix management also trusts `membership.role`, not `training_matrix:read/write`.
- Employees and products APIs are scoped to organization, but not to action-level authority inside that organization.
## Direct URL Assessment
Direct URL behavior is mixed:
- `/dashboard/**` is authentication-protected by `src/proxy.ts`, but authorization still depends on page-level guards.
- `/dashboard/training-matrix` has no page guard, so an authenticated user can load the page shell directly even though its data API later blocks non-admin membership roles.
- Several APIs are missing proxy-level authentication coverage entirely, including:
- `/api/online-lessons/**`
- `/api/permission-templates/**`
- `/api/user-permissions/**`
- `/api/permissions/catalog`
- `/api/permission-audit-logs`
- Those routes appear to rely on route-handler guards instead of proxy enforcement. That is acceptable for security if every handler is guarded, but it is inconsistent with the rest of the API surface and increases review burden.
## UI vs API Authorization Consistency
Main inconsistencies:
- Employee directory UI is HRD-only at page level, but `/api/employees` only requires organization membership.
- Training matrix nav suggests HRD/IT management access, but the page has no server guard and the API uses membership-role checks only.
- Reports nav is HRD-only, but the page allows all employee-area users and delegates scope reduction to server data logic.
- Audit logs nav is super-admin only, while page and API enforce `IT` access.
- Organizers nav is super-admin only, while the page uses `isHRD(...)`.
Conclusion:
- Navigation is not a reliable indicator of backend authorization.
- Several pages are stricter or looser than their APIs.
- Permission template rollout is incomplete because UI and API do not consistently consume the same access model.
## Compatibility Mode Findings
Compatibility-mode authorization is still widespread:
- `requireHRD()`
- `requireIT()`
- `requireUserManagementAccess()`
- `canManageTrainingMatrix(membership.role)`
- `canViewAllReports(role)`
- `canReviewTrainingRecords(membership.role)`
- inline `isHRD(...)` page checks
Risk:
- Any path that authorizes from compatibility roles can bypass explicit permission denies from templates or user overrides.
- This is especially important for review, reporting, user management, training matrix, and master-review flows.
## Missing Permission Descriptions
Permission metadata review in `src/features/permission-management/server/permission-management-catalog.ts`:
- Most permission keys have catalog metadata and Thai descriptions.
- `descriptionEn` is not populated for most entries.
- The file content shows text encoding corruption in the stored description strings, which will degrade readability in the permission-management UI and audit output.
## Findings by Severity
### [High] Permission templates and user overrides are not the single source of truth for authorization
**Module:** Cross-cutting authorization
**Location Found:**
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/api/training-matrix/route.ts`
- `src/app/api/master-review/route.ts`
- `src/features/reports/server/report-data.ts`
- `src/app/dashboard/users/page.tsx`
- `src/app/dashboard/employee-directory/page.tsx`
**Details:**
The codebase already resolves `effectivePermissions`, but many sensitive routes still authorize from membership or business-role compatibility logic. That means a permission template deny may not remove access from all paths for the same user.
**Impact:**
- Fine-grained RBAC is unreliable.
- Permission template rollout can produce false confidence.
- Privileged workflows may remain available after explicit deny overrides.
**Evidence from Code:**
- Training matrix uses `canManageTrainingMatrix(membership.role)`.
- Reports use `canViewAllReports(role)`.
- Master review uses `requireHRD()`.
- User management uses `requireUserManagementAccess()` which maps to `IT`.
**Example Scenario:**
An admin-style member receives a template that removes report-wide access or training-matrix management. The user can still pass role-based guards because the route trusts membership role instead of `effectivePermissions`.
**Recommendation:**
Standardize all sensitive server-side authorization on `requirePermission`, `requireAnyPermission`, or `requireAllPermissions`, and reserve compatibility helpers only for temporary transitional wrappers.
**Must Fix Before Production:** Yes
### [High] Employee and product management APIs allow any organization member to perform management actions by direct API access
**Module:** Employee Management, Products
**Location Found:**
- `src/app/api/employees/route.ts`
- `src/app/api/products/route.ts`
**Details:**
Both APIs only require `requireOrganizationAccess()`. There is no action-level permission check for list or create operations.
**Impact:**
- Any authenticated member in the same organization can query or create records through direct API calls even if the dashboard UI hides those actions.
- This is a true UI-vs-API authorization gap.
**Evidence from Code:**
- `GET /api/employees` and `POST /api/employees` have no `employee_directory:*` or import/review permission gate.
- `GET /api/products` and `POST /api/products` have no product-specific privilege gate.
**Example Scenario:**
A standard employee cannot reach the employee directory through nav, but can still call `/api/employees` directly once authenticated in the same organization.
**Recommendation:**
Add server-side action gates for every management API and align them with the intended permission model.
**Must Fix Before Production:** Yes
### [High] Reports authorization and scope are driven by membership role instead of report permissions
**Module:** Reports
**Location Found:**
- `src/features/reports/server/report-data.ts`
- `src/app/api/reports/export/route.ts`
- `src/app/dashboard/reports/page.tsx`
**Details:**
`canViewAllReports(role)` grants organization-wide reporting only to membership `owner` or `admin`. The actual permission keys `reports:self_read`, `reports:organization_read`, and `reports:export*` do not control the real scope boundary.
**Impact:**
- Report permissions are partially decorative.
- Templates cannot precisely grant or revoke report scope independent of membership role.
- Export rules depend on role-derived access rather than permission-derived access.
**Evidence from Code:**
- `getReportsAccess()` derives `canViewAll` from `canViewAllReports(scope.role)`.
- Export route blocks non-organization-wide users from all reports except `employeeTranscript` using `canViewAll`, not `reports:organization_read`.
**Example Scenario:**
A user granted `reports:organization_read` through a template but holding membership `member` still cannot access org-wide reports, while an `admin` membership can.
**Recommendation:**
Replace report scope decisions with explicit report permissions and keep role defaults only as template seed values.
**Must Fix Before Production:** Yes
### [Medium] Navigation, page guards, and API guards are inconsistent across key modules
**Module:** Navigation and dashboard access
**Location Found:**
- `src/config/nav-config.ts`
- `src/hooks/use-nav.ts`
- `src/app/dashboard/audit-logs/page.tsx`
- `src/app/dashboard/organizers/page.tsx`
- `src/app/dashboard/reports/page.tsx`
- `src/app/dashboard/training-matrix/page.tsx`
**Details:**
The same feature is often gated differently in nav, page render, and API access.
**Impact:**
- Users get confusing UX.
- Direct URL behavior is harder to reason about.
- Authorization reviews become more error-prone.
**Recommendation:**
Define one canonical access contract per module and apply it consistently to nav visibility, page guard, and API handlers.
**Must Fix Before Production:** Consider
### [Medium] Super-admin organization access mutates membership data as a side effect of authorization
**Module:** Session and organization access
**Location Found:**
- `src/lib/auth/session.ts`
**Details:**
`requireOrganizationAccess()` automatically inserts an `admin` membership when a `super_admin` selects an organization without an existing membership.
**Impact:**
- Authorization has write-side effects.
- Auditability and least-privilege reasoning become harder.
- Accidental access expansion is possible through normal navigation.
**Recommendation:**
Separate "temporary super-admin impersonation/access" from persistent membership creation, or make membership creation an explicit administrative action.
**Must Fix Before Production:** Consider
### [Low] Permission catalog descriptions have metadata quality issues
**Module:** Permission Management
**Location Found:**
- `src/features/permission-management/server/permission-management-catalog.ts`
**Details:**
Descriptions exist, but `descriptionEn` is mostly absent and Thai strings appear encoding-corrupted in the file.
**Impact:**
- Permission management UX and audit readability are reduced.
- Not a direct security failure.
**Recommendation:**
Normalize encoding and complete optional description metadata where needed.
**Must Fix Before Production:** No
## Privilege Escalation Risks
Primary privilege-escalation paths observed:
1. Direct API access to `/api/employees` and `/api/products` by ordinary org members.
2. Role-based fallback that ignores explicit permission denies in templates/overrides.
3. Super-admin membership auto-creation inside `requireOrganizationAccess()`.
## Recommended Remediation Order
1. Lock down management APIs that currently require only organization membership.
2. Convert sensitive server routes from compatibility-role checks to permission-first checks.
3. Redefine reports scope and export authorization around `reports:*` permissions.
4. Add missing page guards, especially for `training-matrix`, and align page/API/nav contracts.
5. Remove authorization side effects from super-admin organization access.
6. Clean up permission catalog metadata and encoding.
## Files Reviewed
- `plans/tms-system-full-audit-prompts.md`
- `src/config/nav-config.ts`
- `src/hooks/use-nav.ts`
- `src/components/layout/app-sidebar.tsx`
- `src/proxy.ts`
- `src/lib/auth/session.ts`
- `src/lib/auth/page-guards.ts`
- `src/lib/auth/roles.ts`
- `src/lib/auth/authorization.ts`
- `src/lib/auth/authorization.server.ts`
- `src/lib/auth/permissions.ts`
- `src/app/dashboard/users/page.tsx`
- `src/app/dashboard/employee-directory/page.tsx`
- `src/app/dashboard/training-matrix/page.tsx`
- `src/app/dashboard/reports/page.tsx`
- `src/app/dashboard/audit-logs/page.tsx`
- `src/app/dashboard/organizers/page.tsx`
- `src/app/dashboard/training-policy/page.tsx`
- `src/app/dashboard/permissions/templates/page.tsx`
- `src/app/dashboard/permissions/users/page.tsx`
- `src/app/dashboard/permissions/audit-logs/page.tsx`
- `src/app/api/users/route.ts`
- `src/app/api/employees/route.ts`
- `src/app/api/products/route.ts`
- `src/app/api/training-matrix/route.ts`
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/api/reports/export/route.ts`
- `src/app/api/audit-logs/route.ts`
- `src/app/api/audit-logs/export/route.ts`
- `src/app/api/training-policies/route.ts`
- `src/app/api/master-review/route.ts`
- `src/app/api/notifications/route.ts`
- `src/app/api/permission-templates/route.ts`
- `src/app/api/permissions/catalog/route.ts`
- `src/features/reports/server/report-data.ts`
- `src/features/training-matrix/server/training-matrix-data.ts`
- `src/features/permission-management/server/permission-management-catalog.ts`
## Areas Not Verified
- Runtime browser behavior for redirects, hidden actions, and toast/error handling
- Full API execution against seeded users for every role/template combination
- Exact UI rendering impact of permission catalog encoding issues
- Online lessons, announcements, and user-permission routes at full action-by-action depth
- Whether all non-proxy-covered API routes always apply equivalent route-handler guards in every nested handler

View File

@@ -0,0 +1,424 @@
# Phase 4 - UI/UX & Design System Audit
## Executive Summary
The dashboard already has a usable shared UI foundation through `PageContainer`, shadcn primitives, shared table utilities, and consistent card-based page composition. Responsive handling is generally better than average for an admin system because many key layouts already use `min-w-0`, wrapping button groups, and scoped table overflow.
The largest UI/UX problem is not layout instability but system inconsistency. Several feature areas still diverge from the intended design system, especially reports tables, file-upload flows, heading structure, and copy quality. The most visible issue across the entire UI is widespread Thai text encoding corruption, which materially harms readability, trust, and learnability.
## Screen Inventory
Primary screens reviewed:
- Dashboard shell and sidebar
- Organization switcher and user menu
- Employee directory list
- Training records form and review-oriented detail states
- Announcements form and detail view
- Online lessons form
- Reports page
- Notifications page
- Audit logs table
- Shared page header, heading, data table, toolbar, pagination, notification card
## Layout Assessment
Strengths:
- `src/components/layout/page-container.tsx` gives pages a stable shell with responsive spacing and full-width containment.
- Shared tables correctly scope horizontal overflow inside the table shell instead of the full page.
- Header actions commonly wrap well on small screens.
Weaknesses:
- Header patterns are inconsistent. Some pages use `PageContainer` header only, while others inject another local hero/header block inside the page body, creating double-header behavior.
- Reports stack multiple large cards and wide tables vertically, which is workable but visually heavy and cognitively dense.
- Access fallback inside `PageContainer` is centered plain text rather than a full empty/error-state pattern.
## Typography Assessment
Strengths:
- Core sizes are mostly sensible: large page titles, smaller descriptions, readable card body text.
- Tables generally keep compact admin-dashboard typography.
Weaknesses:
- Typography hierarchy is semantically inconsistent because `Heading` renders `h2` while feature pages often behave like top-level page titles.
- Some screens add their own `h1` under `PageContainer`, producing mixed heading structures across modules.
- Thai text corruption appears in navigation, headers, descriptions, pagination labels, and empty states across many files, which is the most serious typography/content issue in the system.
- Copy style mixes Thai and English unevenly, for example `PageContainer` access fallback remains English while most feature UI is Thai.
## Color & Status Assessment
Strengths:
- Shared `Badge` usage is established for workflow and report states.
- Content approval states appear to reuse shared variants through `content-approval` helpers.
Weaknesses:
- Status language is not fully standardized across features. Similar states use different wording such as archive/hide/remove-publish semantics depending on module.
- Some feedback and button copy rely on variant differences only, without additional structural separation for risky actions.
## Button Standard Assessment
Strengths:
- Most primary/secondary actions use shared `Button` variants.
- Mobile width handling is good in many forms and headers through `w-full sm:w-auto`.
- Action ordering is usually reasonable: cancel/back separated from primary submit action.
Weaknesses:
- Workflow buttons are not standardized across content modules. Announcements and online lessons use similar but not fully shared action group patterns.
- Some significant actions such as publish/archive are triggered directly from detail views without explicit confirmation, which weakens safety and workflow clarity.
- Loading labels are inconsistent and often use `...` instead of a consistent loading copy pattern.
## Table Standard Assessment
Strengths:
- Shared `DataTable`, toolbar, and pagination are good system-level primitives.
- Canonical table modules like employee directory and audit logs follow the shared pattern well.
- Action columns are generally handled in a stable way with pinning and reusable menus.
Weaknesses:
- Reports bypass the shared DataTable stack entirely and use manual `Table` cards, so sorting, pagination, column management, and interaction patterns differ from the rest of the app.
- Manual report tables rely on horizontal scrolling only and do not offer the stronger affordances already available in the shared table system.
- Table empty/loading states are reasonably present, but wording and visual treatment vary by module.
## Form Standard Assessment
Strengths:
- Major forms use TanStack Form wrappers and shared field primitives.
- Validation errors are shown inline and `scrollToFirstError()` is used in important flows.
- Training records form handles employee vs HRD mode clearly.
Weaknesses:
- Required mark style is inconsistent. Some screens use literal `*`, some use styled destructive spans, and some rely on validation only.
- Placeholder and helper-text patterns vary widely between modules.
- Inputs often omit `name` and `autocomplete` attributes, which is a lower-level accessibility and usability gap against modern form guidelines.
- Feature forms mix shared field wrappers with raw HTML elements such as custom `textarea`, creating slightly different spacing and behavior.
## File Attachment UX Assessment
Strengths:
- Training records use a shared `FileUploader` and attachment list, which gives a clearer system pattern.
- Content forms usually show current file, replace/remove flows, and post-save expectations.
Weaknesses:
- Attachment UX is fragmented across modules:
- Training records use shared uploader + attachment list
- Announcements use a raw file input
- Online lessons use custom `AssetPicker` sections
- This creates different expectations for drag-and-drop, file preview, replacement, and removal across similar content workflows.
- View-mode attachment presentation is not standardized across modules.
## Feedback State Assessment
Strengths:
- Most reviewed modules include loading, error, and empty states.
- Toasts are used consistently for mutation feedback.
- Audit logs and notifications have decent empty-state handling.
Weaknesses:
- Loading states are inconsistent between skeleton-style, spinner-in-card, and plain text messaging.
- `PageContainer` access denial uses a plain centered sentence rather than a richer restricted-state pattern.
- Success and error messaging quality varies and is harder to read because of copy encoding issues.
## Dialog / Sheet / Drawer Assessment
Observed strengths:
- Shared dialog/sheet primitives are clearly part of the system.
- Content review uses a shared action dialog component rather than ad hoc per-page modal markup.
Observed gaps:
- This audit did not fully validate focus trap, escape handling, and mobile height behavior for every dialog/sheet implementation.
- Action-safety patterns are not uniform; some important transitions use dialogs while other significant actions run immediately.
## Workflow UX Assessment
Strengths:
- Training records and content modules expose state-aware actions instead of showing every action at once.
- Review notes and current status are surfaced in key detail/form views.
Weaknesses:
- Reports page is functionally useful but visually dense; it reads like stacked exports rather than a guided reporting workspace.
- Content modules blur the line between draft editing, review transition, publish, and archive actions because button patterns are similar but not always grouped by risk or workflow stage.
- Employee directory uses an extra local page hero inside `PageContainer`, which makes it feel visually different from other management screens.
## Responsive Assessment
Strengths:
- Shared layouts use `min-w-0`, wrapped actions, and scoped overflow correctly.
- Employee directory and shared tables are intentionally built for horizontal table scroll on smaller screens.
- Forms generally collapse to a single column well.
Weaknesses:
- Reports is the most likely responsive pain point because of multiple wide manual tables on one page.
- Some custom card headers and action groups depend on wrapping rather than a stronger mobile-specific layout hierarchy.
- Sidebar behavior was not visually runtime-tested in browser during this audit.
## Accessibility Assessment
Positive observations:
- Many interactive controls use semantic elements such as `button`, `Link`, and form labels.
- Some icon-only actions already have `aria-label`, for example notification mark-as-read and table pagination buttons.
- Inline validation patterns exist in major forms.
Key gaps:
- No verified skip-link pattern was found for the dashboard shell.
- Decorative icons are widely rendered without explicit `aria-hidden`, which can add noise for assistive tech depending on the icon implementation.
- Heading hierarchy is inconsistent at the page level because of mixed `h1`/`h2` usage.
- Native form enhancements such as `autocomplete`, input naming consistency, and copy punctuation are not standardized.
- Notifications pagination uses `href='#'` plus click interception, which is weaker semantically than true navigation controls.
## Design System Inconsistencies
Most important system drifts:
1. Copy quality and encoding
- Thai labels are corrupted across multiple feature surfaces.
2. Page header structure
- Shared page shell exists, but some modules duplicate internal hero headers.
3. Table architecture
- Canonical modules use shared DataTable; reports does not.
4. File upload pattern
- Different modules use different uploader UX for similar tasks.
5. Form semantics
- Required markers, placeholders, descriptions, and input metadata are inconsistent.
6. Workflow action treatment
- Some risky actions confirm through dialogs; others execute directly.
## Findings by Severity
### [High] Thai UI copy is encoding-corrupted across major navigation, form, table, and feedback surfaces
**Module:**
Cross-cutting UI copy
**Location Found:**
- `src/config/nav-config.ts`
- `src/components/layout/page-container.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/features/employee-directory/components/employee-directory-header.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/notifications/components/notifications-page.tsx`
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/announcements/components/announcement-view-page.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/online-lessons/components/online-lesson-form.tsx`
**Details:**
Large portions of user-facing Thai text render as mojibake-like corrupted strings instead of readable Thai copy.
**Impact:**
- Severely damages readability and trust.
- Makes workflows harder to learn.
- Reduces accessibility because labels and feedback become harder to interpret.
**Evidence from Code:**
Corrupted Thai strings appear in page titles, descriptions, nav items, buttons, empty states, and pagination labels.
**Example Scenario:**
A user opening navigation, forms, or reports sees broken Thai labels in core actions and status descriptions, making the system feel unreliable even when the workflow itself works.
**Recommendation:**
Normalize file encoding and re-validate every user-facing Thai string before expanding UI work.
**Must Fix Before Production:** Yes
### [Medium] Heading and page-header patterns are semantically and visually inconsistent across modules
**Module:**
Shared layout and page composition
**Location Found:**
- `src/components/ui/heading.tsx`
- `src/components/layout/page-container.tsx`
- `src/features/employee-directory/components/employee-directory-header.tsx`
**Details:**
`Heading` renders `h2`, while some screens also introduce their own `h1` inside the page body. Other screens rely entirely on the shared heading component.
**Impact:**
- Inconsistent visual rhythm across pages.
- Weaker accessibility and document structure.
- Makes it harder to standardize page templates.
**Recommendation:**
Define one canonical page-title pattern for dashboard screens and apply it uniformly.
**Must Fix Before Production:** Consider
### [Medium] Reports UI diverges from the shared table system and creates a noticeably different interaction model
**Module:**
Reports
**Location Found:**
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/components/report-table-card.tsx`
**Details:**
Reports use manual tables instead of the shared DataTable stack used elsewhere in the dashboard.
**Impact:**
- Table behavior differs from the rest of the product.
- Large report cards become dense and harder to scan.
- Users lose familiar controls such as unified table toolbars and standardized interaction patterns.
**Recommendation:**
Either formally designate reports as a separate reporting presentation pattern, or migrate the module toward shared table conventions where practical.
**Must Fix Before Production:** Consider
### [Medium] File attachment UX is fragmented across modules instead of following one reusable pattern
**Module:**
Training Records, Announcements, Online Lessons
**Location Found:**
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/online-lessons/components/online-lesson-form.tsx`
**Details:**
Comparable upload tasks use different components and different interaction rules.
**Impact:**
- Increases cognitive load when users move between modules.
- Makes future improvements and accessibility hardening harder to apply consistently.
**Recommendation:**
Standardize a single attachment/upload UX contract for content and training modules, even if internal storage logic remains feature-specific.
**Must Fix Before Production:** Consider
### [Low] Form and pagination semantics miss several modern UX/a11y conventions
**Module:**
Shared forms and notifications
**Location Found:**
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/online-lessons/components/online-lesson-form.tsx`
- `src/features/notifications/components/notifications-page.tsx`
**Details:**
Common gaps include missing `autocomplete` conventions, inconsistent input metadata, and pagination links implemented with `href='#'` plus click interception.
**Impact:**
- Mostly incremental usability and accessibility debt rather than immediate blockers.
**Recommendation:**
Add a lightweight shared checklist for form semantics, link semantics, and icon accessibility before future UI expansion.
**Must Fix Before Production:** No
## Recommended UI Standard
Suggested system rules for this repo:
1. One dashboard page header pattern
- Top-level page title, description, optional info button, optional right-side action area.
2. One table family
- Use shared DataTable unless a page is intentionally a reporting surface with documented exceptions.
3. One attachment pattern
- Shared file state, current file preview, replace/remove actions, and empty state wording.
4. One required-field style
- Same marker, same placement, same helper/error rhythm.
5. One workflow action grouping rule
- Primary action, secondary action, destructive action, and confirmation behavior defined centrally.
6. One language-quality rule
- All user-facing Thai strings must be UTF-safe, readable, and consistently localized.
## Recommended Remediation Order
1. Fix Thai text encoding across all user-facing surfaces.
2. Standardize heading semantics and page-header composition.
3. Decide whether reports is an intentional exception or should move closer to the shared table pattern.
4. Unify attachment UX across training records, announcements, and online lessons.
5. Add a small accessibility/design checklist for forms, icon buttons, and navigation semantics.
## Files Reviewed
- `plans/tms-system-full-audit-prompts.md`
- `docs/AI_DEVELOPMENT_GUIDE.md`
- `docs/PROJECT_ARCHITECTURE.md`
- `src/components/layout/page-container.tsx`
- `src/components/layout/app-sidebar.tsx`
- `src/components/layout/user-nav.tsx`
- `src/components/org-switcher.tsx`
- `src/components/ui/heading.tsx`
- `src/components/ui/notification-card.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-toolbar.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/config/nav-config.ts`
- `src/features/employee-directory/components/employee-directory-header.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/announcements/components/announcement-view-page.tsx`
- `src/features/announcements/components/announcement-status-badge.tsx`
- `src/features/online-lessons/components/online-lesson-form.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/components/report-table-card.tsx`
- `src/features/notifications/components/notifications-page.tsx`
- `src/features/audit-logs/components/audit-log-listing.tsx`
- `src/features/audit-logs/components/audit-log-table.tsx`
- `src/features/content-approval/components/content-approval-status-badge.tsx`
## Areas Not Verified
- Runtime browser validation of focus order, keyboard traversal, and screen-reader output
- Visual contrast measurement against rendered theme tokens
- All dialogs/sheets/drawers at runtime, including focus trap and mobile height behavior
- Full mobile behavior of the sidebar and complex tables in a live viewport
- Every feature module in the repo; this review focused on canonical and high-traffic dashboard surfaces

View File

@@ -0,0 +1,352 @@
# Phase 5 - Code Quality & Performance Audit
## Executive Summary
The system currently passes `typecheck`, `lint`, and production `build`, so the codebase is operational and deployable in its current state. The main risk in this phase is not immediate build failure, but accumulated maintainability and performance debt concentrated in several oversized route handlers, server data modules, and workflow-heavy client forms.
The most important quality issue is structural complexity. A small set of files currently own too many responsibilities at once: authorization-aware data loading, filtering, export generation, workflow transitions, storage handling, notifications, and UI orchestration. This increases regression risk, makes optimization harder, and reduces testability.
The most important performance issue is repeated work inside large reporting and workflow paths rather than obvious catastrophic hotspots. In particular, report generation repeats scope-resolution queries, export logic is tightly coupled to dataset construction, and several large client components keep broad interactive state on the client without further decomposition.
## Build/Config Assessment
### Observed configuration
- `package.json:10-15` defines `dev`, `build`, `lint`, `lint:fix`, and `lint:strict`.
- There is no dedicated `typecheck` script in `package.json`.
- `tsconfig.json:5-19` uses `strict: true`, `isolatedModules: true`, and `incremental: true`.
- `tsconfig.json:5-6` also keeps `allowJs: true` and `skipLibCheck: true`.
- Linting is done by `oxlint`, not a stricter combined lint-plus-type gate.
### Assessment
Strengths:
- The project already compiles cleanly under production build.
- TypeScript strict mode is enabled.
- The repo has a stricter lint mode available through `lint:strict`.
Weaknesses:
- Missing `typecheck` script makes CI and contributor workflows less explicit.
- `allowJs: true` weakens long-term type-safety expectations in a TypeScript-first codebase.
- `skipLibCheck: true` is a practical default, but it can hide dependency typing drift.
- The default lint command allows warnings to pass, so code-quality drift can accumulate silently.
## TypeScript Assessment
### Strengths
- `npx tsc --noEmit` completed successfully.
- Most feature modules use explicit contracts and typed query/mutation boundaries.
- Shared data access patterns appear to prefer typed service layers over untyped ad hoc fetches.
### Findings
- `src/instrumentation-client.ts` uses `(Sentry as any)`, which weakens observability-related type safety.
- `src/components/forms/demo-form.tsx` and `src/features/products/schemas/product.ts` use permissive `any`-style schema patterns, suggesting leftover demo/legacy looseness.
- `src/features/forms/components/sheet-product-form.tsx` uses `productSchema as any`, which bypasses compiler guarantees.
- `src/lib/api-client.ts:57` throws generic `Error` instances instead of a typed API/domain error model.
### Assessment
TypeScript quality is acceptable at the baseline level, but not yet fully mature. The compiler settings are strong enough to prevent many failures, while a small set of escape hatches and generic error shapes still reduce confidence in cross-layer correctness.
## React Assessment
### Strengths
- The app reuses shared primitives and a shared query client.
- Several flows use TanStack Query instead of bespoke client fetching logic.
- Shared table state is centralized in `src/hooks/use-data-table.ts`, which is a good reuse decision even though the hook is now quite complex.
### Findings
- `src/features/training-records/components/training-record-form.tsx` is 765 lines and combines form orchestration, uploads, mutations, attachment state, and workflow logic in one client component.
- `src/features/online-lessons/components/online-lesson-form.tsx` is 554 lines and keeps many local file/removal states plus multiple workflow mutations in one component.
- `src/features/announcements/components/announcement-form.tsx` uses the same multi-mutation workflow pattern and is trending toward the same shape.
- `src/features/reports/components/reports-page-content.tsx:278,317` triggers `react(no-unstable-nested-components)` warnings.
- `src/components/ui/calendar.tsx:58` and `src/components/kbar/render-result.tsx:10` also define nested components during render.
### Assessment
The main React risk is not hook misuse at scale, but oversized client components with broad responsibility and broad rerender surfaces. These components are harder to reason about, harder to profile, and harder to unit test than smaller view-model plus presentational splits.
## Next.js Assessment
### Strengths
- Production build succeeded under Next.js 16.2.6.
- The app makes meaningful use of server-side data loading.
- `src/features/overview/server/overview-data.ts:171,185,249,519` uses `cache()` well in several high-read paths.
### Findings
- `src/app/api/announcements/[id]/route.ts` is 683 lines and combines multipart parsing, draft/save/submit transitions, storage operations, audit logging, and delete handling in a single route module.
- `src/lib/api-client.ts:29-41` resolves server request headers and reconstructs origin dynamically on every server request path, which adds per-call overhead and keeps the route-handler pattern tightly coupled to internal HTTP.
- The build output still includes many legacy/demo screens and routes such as `/dashboard/forms`, `/dashboard/kanban`, `/dashboard/chat`, `/dashboard/product`, and `/dashboard/employees`, which increases bundle and maintenance surface.
### Assessment
The app is using Next.js capably, but it is not yet taking full advantage of cleaner server boundaries. The biggest framework-level opportunity is reducing workflow complexity inside route handlers and separating internal server logic from HTTP transport concerns.
## Validation Assessment
### Strengths
- The broader codebase follows a Zod-plus-form-wrapper approach.
- Workflow modules show evidence of server-side validation before persistence.
### Findings
- Validation quality varies by feature. Demo and product-related areas still contain permissive schema patterns such as `.any()`.
- File-heavy workflow routes appear to validate through branching procedural logic inside route handlers rather than through smaller reusable validation units.
- Status/date/file validation logic appears repeated across announcement and online-lesson workflow code paths instead of being fully centralized.
### Assessment
Validation is good enough for core runtime flows, but consistency is not uniform across the repo. The main weakness is schema fragmentation and procedural validation logic inside large handlers.
## Error Handling Assessment
### Strengths
- Auth/session helpers throw explicit `AuthError` instances for unauthorized and forbidden cases.
- Route handlers generally return structured JSON responses rather than raw exceptions.
### Findings
- `src/lib/api-client.ts:57` throws plain `Error`, collapsing HTTP status, domain code, and user-display concerns into one generic error shape.
- `src/lib/auth/session.ts` contains many repeated `throw new AuthError(...)` branches, which is clear but repetitive and hints at opportunities for more composable authorization guards.
- Large route handlers and data modules mix business logic with logging and side effects, which makes it harder to guarantee consistent error mapping.
### Assessment
Error handling is functional but not yet strongly normalized. The biggest gap is the lack of a consistent typed application error model spanning route handlers, services, and client consumers.
## Duplicate Code Assessment
### Findings
- Date formatting is scattered across many files through direct `new Date(...).toISOString()` and `toLocaleDateString(...)` usage, including `src/app/api/announcements/route.ts`, `src/app/api/announcements/[id]/route.ts`, `src/app/api/online-lessons/[id]/route.ts`, `src/app/api/audit-logs/export/route.ts`, and `src/features/reports/components/reports-page-content.tsx:78-83`.
- Workflow patterns for draft/save/submit/approve/archive actions are conceptually similar between announcements and online lessons, but remain implemented separately.
- Report rendering repeats table-like configuration and inline mapping logic inside `src/features/reports/components/reports-page-content.tsx`.
- Authorization guard usage is centralized, but authorization-related branching still appears repeatedly inside high-level server modules.
### Assessment
The repo has good reuse in core scaffolding, but duplication is still visible in domain workflows, date formatting, report presentation, and status transition logic. This is manageable now, but it will get more expensive as more content modules are added.
## Naming & Convention Assessment
### Strengths
- Feature and file naming is generally readable and follows the project structure.
- Route names align well with feature intent.
### Findings
- `src/components/ui/slider.tsx:16` uses `_values`, which triggers a lint naming warning.
- Mixed legacy/demo/runtime naming remains across the repo, especially in `products`, `forms`, `chat`, `kanban`, and old `employees` areas.
- Some modules use domain-accurate names but still house too many concerns for the file name to communicate actual scope, for example `report-data.ts` and `overview-data.ts`.
### Assessment
Naming is mostly not a blocking issue. The larger convention problem is scope drift: files whose names imply a focused responsibility but whose implementations have become mini-subsystems.
## Database Query Performance Assessment
### Strengths
- Several aggregation-heavy paths use SQL composition and `Promise.all` rather than purely sequential querying.
- Overview data makes a meaningful attempt to cache expensive scope/filter work.
### Findings
- `src/features/reports/server/report-data.ts:126,247,308,386` repeatedly calls `getScopedEmployeeIds(...)` in multiple report builders, which risks repeated scope-resolution queries within one report/export request.
- `src/features/reports/server/report-data.ts:314,567,621` mixes broad dataset fetching with export preparation and follow-up joins, making it harder to deduplicate query work.
- `src/features/permission-management/server/user-permission-data.ts:375` relies on transactional app logic to preserve one-active-template behavior, while the file itself contains a TODO noting the absence of a database-level unique constraint.
- Large export paths in reports include PDF and Excel generation in the same module as query logic, increasing the chance of over-fetching and memory-heavy processing.
### Assessment
The main database-performance risk is repeated query work in analytics/reporting flows rather than obvious N+1 loops in everyday CRUD pages. Reporting will likely be the first area to degrade under scale.
## Frontend Performance Assessment
### Strengths
- Shared data-table state management includes debounce support and URL-state integration.
- Many pages are server-rendered first and hydrate into targeted client interactivity rather than fully client-side shells.
### Findings
- `src/features/training-records/components/training-record-form.tsx`, `src/features/online-lessons/components/online-lesson-form.tsx`, and `src/features/announcements/components/announcement-form.tsx` keep large interactive workflows entirely in single client components.
- `src/features/reports/components/reports-page-content.tsx` contains nested render-time component definitions flagged by lint, which can cause unnecessary remounting and rerender churn.
- The build output still contains many legacy/demo dashboard surfaces, which increases long-term client bundle pressure even if each page is route-split.
- Heavy UI packages and legacy dashboard features remain installed even where they do not appear central to the TMS runtime.
### Assessment
Frontend performance risk is moderate. The biggest concerns are rerender breadth and bundle sprawl, not obviously broken runtime performance today.
## API Performance Assessment
### Strengths
- The production build completed successfully, which suggests current route composition is at least operational.
- Many server modules already use batching through `Promise.all`.
### Findings
- `src/app/api/announcements/[id]/route.ts:88,162-623` performs multipart parsing, state transition branching, storage actions, and many audit log writes in one path.
- The same route logs multiple events in sequence at `445,458,475,522,549,573,609,623`, and only one section uses `Promise.all` at `492`.
- `src/app/api/online-lessons/[id]/route.ts` follows a similar content-workflow route shape, increasing the chance of repeated sequential I/O.
- `src/lib/api-client.ts:41` routes internal feature services through HTTP fetch calls, which is acceptable for consistency but adds overhead versus direct server-side composition when both sides live in the same app.
### Assessment
API performance is acceptable for current scale, but workflow-heavy content endpoints are likely to become latency hotspots because they combine validation, persistence, storage, notification, and audit concerns in one request lifecycle.
## Dead Code Assessment
### Findings
- `npm run lint` reports unused imports and variables in active code:
- `src/components/ui/duration-picker.tsx:13`
- `src/features/online-lessons/components/online-lesson-form.tsx:121`
- `src/features/organizers/components/organizers-page.tsx:31`
- `src/features/reports/components/reports-page-content.tsx:120`
- `src/app/api/online-lessons/[id]/route.ts:240`
- `src/features/reports/server/report-data.ts:8,11,12`
- The build output confirms many legacy/demo pages and features are still present in the shipped app tree.
- `src/constants/mock-api.ts` and `src/constants/mock-api-users.ts` still exist and receive lint warnings, indicating legacy/demo residue remains in the repository.
### Assessment
Dead code is not overwhelming, but there is enough residue to create noise, hide real issues, and expand the maintenance surface beyond the actual TMS runtime.
## Testability Assessment
### Strengths
- Some data access is already separated into feature-level server modules.
- Shared hooks and shared auth helpers create a starting point for isolated testing.
### Findings
- `src/features/reports/server/report-data.ts` couples filtering, authorization scoping, querying, table shaping, and export generation in one file.
- `src/features/overview/server/overview-data.ts` couples multiple dashboard datasets and policy/filter logic in one file despite good cache usage.
- `src/app/api/announcements/[id]/route.ts` mixes multipart parsing, workflow policy, storage, database updates, notifications, and auditing, which is difficult to unit test without broad integration scaffolding.
- `src/hooks/use-data-table.ts` is valuable shared infrastructure, but its breadth makes it a likely source of subtle regressions and harder isolated testing.
### Assessment
Testability is the clearest structural weakness after maintainability. The codebase is not short on logic separation everywhere, but its heaviest business flows are still assembled in units that are larger than ideal for focused tests.
## Findings by Severity
### High
1. Oversized workflow handlers and data modules concentrate too many responsibilities.
- Evidence: `src/app/api/announcements/[id]/route.ts` (683 lines), `src/features/reports/server/report-data.ts` (759 lines), `src/features/overview/server/overview-data.ts` (773 lines), `src/features/training-records/components/training-record-form.tsx` (765 lines).
2. Reporting paths repeat scope-resolution work and combine querying with export generation.
- Evidence: `src/features/reports/server/report-data.ts:126,247,308,386,766-822`.
3. Core workflow-heavy client forms are broad client boundaries with large rerender surfaces and low testability.
- Evidence: `training-record-form.tsx`, `online-lesson-form.tsx`, `announcement-form.tsx`.
### Medium
1. Error handling is not yet consistently typed across service and client boundaries.
- Evidence: `src/lib/api-client.ts:57`.
2. Database integrity for active permission-template assignment relies on app logic instead of a DB constraint.
- Evidence: TODO in `src/features/permission-management/server/user-permission-data.ts`, transaction at `375`.
3. Duplicate date formatting and workflow status handling remain spread across many routes and components.
4. Default lint workflow allows warnings to pass, and no dedicated `typecheck` script exists.
5. Legacy/demo modules continue to expand bundle and maintenance surface.
### Low
1. Small unused variables/imports remain in active modules.
2. Naming/convention drift exists in a few UI utility files and legacy/demo areas.
## Quick Wins
1. Add a dedicated `typecheck` script and run it in CI with `lint:strict`.
2. Remove current unused imports/variables reported by `oxlint`.
3. Centralize date formatting helpers for API payloads and Thai display formatting.
4. Extract nested render-time component definitions flagged by lint.
5. Split report export formatting from report data retrieval.
## Structural Improvements
1. Break `report-data.ts` into scoped-query, dataset-builder, and export-renderer modules.
2. Break `overview-data.ts` into dashboard-section loaders plus a smaller composition layer.
3. Move announcement and online-lesson workflow transition logic into dedicated domain services invoked by thin route handlers.
4. Split large client forms into smaller sections with isolated mutation/upload adapters.
5. Introduce a typed application error model shared by route handlers, service callers, and client UI.
6. Audit legacy/demo routes and dependencies for removal or quarantine outside the production dashboard surface.
## Recommended Refactor Order
1. Reports server path
- Highest combined maintainability, query-efficiency, and export-performance payoff.
2. Announcement and online-lesson workflow handlers
- Highest API complexity and side-effect density.
3. Large client workflow forms
- Best follow-up once workflow services are thinner and easier to consume.
4. Error model and date-format normalization
- Broad cross-cutting cleanup with good leverage.
5. Legacy/demo surface reduction
- Useful after core runtime paths are stabilized.
## Commands Executed and Results
- `npx tsc --noEmit`
- Passed.
- `npm run lint`
- Passed with warnings. Key warnings included unused imports/variables and `react(no-unstable-nested-components)`.
- `npm run build`
- Passed on Next.js `16.2.6` with production build and static page generation completed successfully.
- Multiple `rg` and `Select-String` read-only scans
- Used to inspect large files, duplicate patterns, lint targets, and route complexity.
## Files Reviewed
- `package.json`
- `tsconfig.json`
- `src/lib/api-client.ts`
- `src/lib/auth/session.ts`
- `src/lib/query-client.ts`
- `src/hooks/use-data-table.ts`
- `src/features/reports/server/report-data.ts`
- `src/features/overview/server/overview-data.ts`
- `src/features/permission-management/server/user-permission-data.ts`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/online-lessons/components/online-lesson-form.tsx`
- `src/features/announcements/components/announcement-form.tsx`
- `src/app/api/announcements/[id]/route.ts`
- `src/app/api/online-lessons/[id]/route.ts`
- `src/components/ui/calendar.tsx`
- `src/components/ui/duration-picker.tsx`
- `src/components/ui/slider.tsx`
- `src/constants/mock-api.ts`
- `src/constants/mock-api-users.ts`
## Areas Not Verified
- No runtime profiling was performed in a browser or with production tracing, so rerender cost and interaction latency were assessed from structure and lint/tool evidence rather than flamegraphs.
- No database execution plans were captured, so index/selectivity findings are risk-based rather than EXPLAIN-verified.
- No bundle analyzer output was generated, so bundle-size findings are based on route inventory and dependency surface, not measured chunk sizes.
- No automated test suite review was performed because this phase focused on code quality, static analysis, and build/runtime structure.

View File

@@ -0,0 +1,370 @@
# Phase 6 - Security Audit
## Executive Summary
The application has a solid baseline in two important areas: server-side authorization is used broadly across route handlers, and many sensitive workflow actions are audited. The main security risks are not widespread missing auth checks, but a smaller set of high-impact gaps in file delivery, request hardening, dependency posture, and abuse protection.
The most serious application-layer issue is direct public exposure of uploaded online-lesson assets. Uploaded video, attachment, and thumbnail files are stored under `public/uploads/...`, serialized back to clients as direct public URLs, and rendered directly in the UI. This bypasses the guarded download pattern that already exists in other parts of the system.
The second major risk is the import surface. Both spreadsheet import endpoints accept files based only on extension presence, read the entire file into memory, and rely on `xlsx`, which `npm audit` currently reports with high-severity advisories. Combined with the absence of rate limiting and explicit file-size controls on import endpoints, this increases the risk of denial-of-service and unsafe spreadsheet parsing.
## Threat Surface Inventory
Primary security-sensitive surfaces reviewed:
- Auth.js credential login flow
- Session and active-organization switching
- Server-side authorization helpers
- Permission-management APIs
- Announcement, online-lesson, and certificate file handling
- Training-record and employee spreadsheet imports
- Report export endpoints
- Notification and workflow action endpoints
- Next.js runtime/configuration and proxy perimeter
## Authentication Assessment
### Strengths
- `src/auth.ts:25-33` validates login input and uses bcrypt password verification.
- `src/auth.ts:57,81,103,137` records login success and failure events in audit logs.
- Self-service registration is disabled in `src/app/api/auth/register/route.ts`.
- Session enforcement is centralized through `requireSession()`.
### Findings
- `src/auth.ts:33` falls back to `dev-only-auth-secret-change-me` outside production. This is acceptable for local development but risky if a non-production environment is internet-exposed with weak secret hygiene.
- `src/auth.ts:36` sets `trustHost: true`. This is common in proxied deployments, but it raises deployment sensitivity if host/header forwarding is not tightly controlled upstream.
- No brute-force controls, lockout logic, or request throttling were found around credential login.
- Password policy is minimal: sign-in and auth forms enforce only `min(8)`, and `src/features/users/schemas/user.ts` does not enforce password strength itself.
### Assessment
Authentication is functionally correct, but it lacks abuse resistance and stronger operational guardrails.
## Authorization Assessment
### Strengths
- Broad route coverage uses server-side guards such as `requireOrganizationAccess`, `requirePermission`, `requireHRD`, `requireAllPermissions`, and `requireSuperAdminOrganizationAccess`.
- Permission-management endpoints are protected at the route-handler level even though they are not included in the proxy perimeter list.
- Download routes for announcement attachments and training-record certificates enforce organization-aware access checks before reading files from disk.
### Findings
- `src/proxy.ts:4-18,41-56` only protects a subset of API prefixes at the perimeter. Notably, online-lesson and permission-management APIs are not listed there, even though their handlers do perform server-side checks. This is a defense-in-depth gap rather than a direct bypass.
- `src/lib/auth/session.ts` and `src/app/api/organizations/active/route.ts` can auto-create admin memberships for super-admin/HRD organization access. This may be intended, but it is a privileged side effect inside an access path and should be treated carefully.
- Permission-management routes use `requireSuperAdminOrganizationAccess()`, which currently depends on the same organization-access helper that can create fallback memberships.
### Assessment
Authorization is stronger than average for an admin dashboard. The main risk is not broken route protection by default, but privileged side effects embedded inside access-resolution flows.
## Input Validation Assessment
### Strengths
- Core workflow routes use Zod or equivalent server-side validation.
- Online-lesson URLs are constrained to `http/https` via schema validation.
- Route params are commonly parsed and rejected when numeric IDs are invalid.
### Findings
- `src/app/api/training-records/import/route.ts:228-236` and `src/app/api/import-employees/route.ts:460-474` only verify that the upload is a `File` and that the name ends with `.xlsx`; no explicit file-size or MIME validation is enforced before parsing.
- Import endpoints load the full spreadsheet with `await file.arrayBuffer()` before further validation.
- Several workflow routes perform large amounts of procedural validation inline inside route handlers, which makes consistency harder to maintain.
### Assessment
Input validation is decent for routine CRUD, but weaker on large-file and batch-import surfaces.
## Injection Assessment
### Strengths
- Most database access uses Drizzle query builders and parameterized expressions.
- No command-execution patterns or child-process usage were found in application runtime paths.
- Storage helpers sanitize path segments and verify resolved paths stay under upload roots.
### Findings
- `npm audit` reports a high-severity advisory for `drizzle-orm` regarding SQL injection via improperly escaped SQL identifiers in versions `<0.45.2`.
- `src/features/reports/server/report-data.ts:446-555` uses raw SQL composition for reporting. The visible usage appears parameterized, but this area should be considered high-scrutiny because it is the most SQL-heavy path in the app.
- `npm audit` reports high-severity advisories for `xlsx`, which is used to parse untrusted spreadsheet uploads.
- CSV export exists in `src/app/api/audit-logs/export/route.ts`; this phase did not fully verify whether cells beginning with spreadsheet formula prefixes are neutralized.
### Assessment
There is no clear app-authored SQL injection bug in the reviewed code, but dependency-level injection risk is real and currently unresolved.
## XSS Assessment
### Strengths
- Announcement content, review comments, and online-lesson descriptions are rendered as plain text with `whitespace-pre-wrap`, not injected HTML.
- No user-content Markdown or rich-text renderer was found in the reviewed content workflows.
- The single `dangerouslySetInnerHTML` usage in `src/components/ui/chart.tsx` is used to emit generated CSS variables, not user-provided rich text.
### Findings
- Online-lesson external video URLs are rendered into outbound links and embed decisions. The code restricts embeddable iframes to recognized YouTube hosts, which is good, but general outbound URLs are still allowed for non-embeddable links.
- No repo-level Content Security Policy was found in `next.config.ts` or related runtime config.
### Assessment
Direct stored-XSS exposure appears low in the reviewed flows. The larger XSS concern is missing browser-side hardening such as CSP.
## CSRF & Request Protection Assessment
### Strengths
- State-changing operations generally use non-GET methods.
- Auth is session-based and routed through Auth.js rather than custom ad hoc cookies.
### Findings
- No explicit CSRF token validation, origin checks, or referer checks were found in state-changing route handlers.
- No centralized request-hardening middleware was found for mutation endpoints.
- Because cookie behavior is library-managed, SameSite/secure cookie posture was not explicitly verified from local code.
### Assessment
This is primarily a hardening gap, not a confirmed exploit in the reviewed code. Still, the app currently relies heavily on framework defaults and browser cookie behavior rather than explicit anti-CSRF controls.
## File Upload Assessment
### High-risk finding
- `src/lib/online-lesson-storage.ts:36-86` writes uploaded lesson assets to `public/uploads/online-lessons/...` and returns direct public URLs.
- `src/features/online-lessons/server/online-lesson-data.ts:51,53,55` serializes those direct file URLs to clients unchanged.
- `src/features/online-lessons/components/online-lesson-detail-page.tsx:198-200,284-290,328-336` renders those URLs directly in `<img>`, `<video>`, and download/open links.
Impact:
- Any user or external party who obtains one of these URLs can fetch the file directly from the public assets path without passing through authorization checks.
- This creates a real broken-access-control and data-exposure risk for uploaded lesson media and attachments.
### Additional findings
- `src/lib/announcement-storage.ts` and `src/lib/certificate-storage.ts` also store files under `public/uploads/...` and create direct public paths, but the primary application serializers now prefer guarded API download URLs instead of returning the raw storage path to clients.
- No virus scanning, content scanning, or magic-byte verification was found for uploaded files.
- Online-lesson attachments permit several document/archive formats, including ZIP, but are still served from public paths.
### Assessment
File upload handling is the most important application-layer security weakness in the current codebase, especially for online-lesson assets.
## Sensitive Data Assessment
### Strengths
- Many protected download responses use `Cache-Control: private, no-store`.
- Permission and organization access is checked server-side before reading many sensitive records.
### Findings
- Uploaded lesson assets are publicly reachable by URL, which can expose internal training content and attachments.
- Login failure audit logs in `src/auth.ts` persist identifiers and, in one case, user email inside audit payloads. This may be acceptable operationally, but it increases PII footprint inside logs.
- User and employee tables render email, employee code, organization, and department information broadly across admin UIs; this is expected for the app, but it increases the need for stronger export and audit controls.
### Assessment
The biggest sensitive-data problem is file exposure, not routine JSON overexposure in the reviewed API routes.
## Security Headers Assessment
### Findings
- No `headers()` configuration was found in `next.config.ts`.
- No explicit Content Security Policy, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, or HSTS configuration was found in reviewed app config.
- Download routes set `Cache-Control`, but broader site-level browser hardening appears absent.
### Assessment
Browser-side security headers are currently under-configured.
## Rate Limiting Assessment
### Findings
- No rate-limiting or throttling implementation was found for login, imports, exports, uploads, or workflow actions.
- This directly affects:
- Credential login
- Spreadsheet import endpoints
- Report export endpoint
- File upload endpoints
- Notification mutation endpoints
### Assessment
Rate limiting is a notable gap and materially increases brute-force and denial-of-service risk.
## Audit Log Assessment
### Strengths
- Authentication success/failure is logged.
- Permission changes have dedicated audit infrastructure.
- Import operations log audit entries.
- Announcement and workflow transitions show extensive audit logging.
### Findings
- `src/app/api/reports/export/route.ts` does not log report exports, even though exports can contain organization-wide training and employee data.
- This creates an audit gap for sensitive data extraction.
- This phase did not verify logout logging or sensitive-record read auditing beyond the reviewed routes.
### Assessment
Audit coverage is good for mutations, but weaker for data-export visibility.
## Dependency & Configuration Assessment
### `npm audit` results
- Total vulnerabilities: `11`
- High: `3`
- Moderate: `8`
Key findings:
- `drizzle-orm`
- High severity advisory reported by `npm audit`.
- `xlsx`
- High severity advisories reported by `npm audit`.
- Especially relevant because the app parses untrusted spreadsheet uploads in two endpoints.
- `sort-by` / `object-path`
- High/moderate advisory chain reported by `npm audit`.
- `@opentelemetry/core` and related packages
- Moderate memory-allocation advisory reported by `npm audit`.
### Configuration findings
- `src/auth.ts:33` uses a development fallback secret.
- `src/auth.ts:36` sets `trustHost: true`.
- `next.config.ts` removes console logs in production, which is good, but does not add security headers.
### Assessment
Dependency security posture needs attention now, especially because the vulnerable packages are in active code paths rather than unused tooling only.
## Business Logic Security Assessment
### Strengths
- Review/publish/archive flows check content status and permissions before transition.
- Training-record certificate download checks ownership/scope through the parent training record.
### Findings
- Privileged organization switching can materialize new admin memberships as a side effect.
- Report exports are permission-checked, but not audited.
- This phase did not find a clear "publish without approve" bypass in the reviewed content workflows, but the workflow handlers are large enough that they deserve dedicated regression tests.
### Assessment
Business-logic security is generally thoughtful, but several privileged flows remain too implicit and too large for easy verification.
## Findings by Severity
### High
1. Online-lesson uploaded assets are publicly accessible by direct URL.
- Evidence: [online-lesson-storage.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/online-lesson-storage.ts:36), [online-lesson-data.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/online-lessons/server/online-lesson-data.ts:51), [online-lesson-detail-page.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/online-lessons/components/online-lesson-detail-page.tsx:284)
2. Spreadsheet import endpoints accept unbounded `.xlsx` uploads and parse them with a dependency that currently has high-severity advisories.
- Evidence: [training-record import route](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/import/route.ts:228), [employee import route](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/import-employees/route.ts:460), `npm audit` finding for `xlsx`
3. Active dependency vulnerabilities include high-severity advisories in `drizzle-orm` and `xlsx`.
- Evidence: `npm audit --json`
### Medium
1. No brute-force or rate-limiting protections were found for login, upload, import, or export endpoints.
2. No repo-level security headers configuration was found.
3. No explicit CSRF/origin validation layer was found for mutation endpoints.
4. Report exports are not audit-logged.
5. Privileged organization access can create admin memberships as a side effect.
6. Login audit logs increase sensitive identifier/email footprint inside logs.
### Low
1. Proxy perimeter coverage is incomplete, though server-side route guards still protect the affected APIs.
2. Development fallback secret is acceptable locally, but risky if misused in a shared environment.
## Quick Wins
1. Stop returning direct public URLs for online-lesson files; serve them through guarded download/stream routes.
2. Add strict file-size limits and MIME checks to both spreadsheet import endpoints before `arrayBuffer()` parsing.
3. Add rate limiting for `/api/auth/[...nextauth]`, import, export, and upload routes.
4. Add export audit logging for reports.
5. Add baseline browser security headers in Next.js config or edge middleware.
## Structural Improvements
1. Move all sensitive file delivery to authenticated route handlers and eliminate `public/uploads` for protected assets.
2. Introduce a centralized request-hardening layer for origin checking, abuse controls, and common mutation protections.
3. Separate privileged organization-access resolution from membership-creation side effects.
4. Add dedicated security tests for workflow transitions, file access, and export scope enforcement.
5. Review and remediate vulnerable dependencies in a controlled upgrade track, especially `xlsx` and `drizzle-orm`.
## Recommended Remediation Order
1. Online-lesson file exposure
- Highest real-world access-control risk.
2. Import surface hardening
- Add file-size checks and reduce exposure from vulnerable spreadsheet parsing.
3. Dependency remediation
- Prioritize packages in active parsing/data-access paths.
4. Rate limiting and request-hardening
- Best leverage across login, import, export, and uploads.
5. Security headers and audit-gap cleanup
- Important defense-in-depth after the higher-risk access issues are contained.
## Commands Executed and Results
- `rg` and `Select-String` across auth, proxy, storage, API routes, and config
- Used to inspect auth guards, upload paths, security headers, and audit coverage.
- `npm audit --json`
- Reported `11` vulnerabilities total: `3` high and `8` moderate.
## Files Reviewed
- `src/auth.ts`
- `src/proxy.ts`
- `src/lib/auth/session.ts`
- `src/lib/auth/authorization.server.ts`
- `next.config.ts`
- `src/lib/announcement-storage.ts`
- `src/lib/online-lesson-storage.ts`
- `src/lib/certificate-storage.ts`
- `src/app/api/auth/register/route.ts`
- `src/app/api/organizations/active/route.ts`
- `src/app/api/reports/export/route.ts`
- `src/app/api/training-records/import/route.ts`
- `src/app/api/import-employees/route.ts`
- `src/app/api/announcements/[id]/attachment/route.ts`
- `src/app/api/training-records/[id]/certificates/route.ts`
- `src/app/api/training-records/[id]/certificates/[certificateId]/download/route.ts`
- `src/app/api/user-permissions/route.ts`
- `src/app/api/permission-templates/route.ts`
- `src/app/api/permissions/catalog/route.ts`
- `src/features/announcements/server/announcement-data.ts`
- `src/features/online-lessons/server/online-lesson-data.ts`
- `src/features/training-records/server/training-record-data.ts`
- `src/features/online-lessons/schemas/online-lesson.ts`
- `src/features/announcements/components/announcement-view-page.tsx`
- `src/features/online-lessons/components/online-lesson-detail-page.tsx`
- `src/components/ui/chart.tsx`
- `src/features/users/schemas/user.ts`
## Areas Not Verified
- This phase did not perform live penetration testing, browser interception, or runtime CSRF validation against a running deployment.
- Cookie flags were not verified from actual HTTP responses.
- This phase did not exhaustively review every route handler in the repository; it focused on the highest-risk auth, permission, upload, import, and export surfaces.
- Dependency advisories were taken from local `npm audit` output and were not independently triaged against actual reachable exploit paths in this specific deployment model.

View File

@@ -0,0 +1,412 @@
# Phase 7 - Production Readiness Review
## Executive Summary
The codebase is buildable and can likely be deployed in a controlled environment, but it is not yet production-ready as an operational system. The strongest signals in favor of readiness are that typecheck, lint, and production build all succeed, Drizzle migrations exist in source control, Docker artifacts exist, and Sentry instrumentation is wired in.
The strongest signals against readiness are operational rather than purely code-level: there is no real CI pipeline in the repository, no automated test suite, no health-check endpoint, no documented backup/restore process, no documented rollback runbook, and major documentation drift remains from the original starter template. On top of that, the unresolved high-risk security issue from Phase 6 around public online-lesson assets should be treated as a production blocker.
Production decision: **No-Go** in the current state.
## Build Readiness
### Verified commands
- `npm run lint`
- Passed with warnings.
- `npx tsc --noEmit`
- Passed.
- `npm run build`
- Passed on Next.js `16.2.6`.
- `npm test`
- Failed because no `test` script exists.
### Strengths
- `package.json` includes `build`, `start`, `lint`, migration, and seed scripts.
- `.nvmrc` pins Node major version `22`.
- Production build completed successfully and emitted the expected standalone-compatible output tree.
### Findings
- There is no `test` script in `package.json`.
- There is no dedicated `typecheck` script in `package.json`, even though `tsc` itself passes.
- `npm run lint` passes only with warnings; the default path does not gate warning-free readiness.
- Production `start` script exists, but this review did not perform a live runtime smoke test of `next start`.
### Assessment
**Partially Ready**. Buildability is good, but release gating is incomplete.
## Environment Readiness
### Strengths
- `env.example.txt` documents a minimal environment contract:
- `AUTH_SECRET`
- `DATABASE_URL`
- optional Sentry variables
- `BUILD_STANDALONE`
- `drizzle.config.ts` explicitly fails when `DATABASE_URL` is missing.
### Findings
- No central runtime environment validation schema was found.
- Missing-variable behavior is inconsistent across the app; some values fail fast while others silently fall back.
- `src/auth.ts` still contains a non-production fallback secret value (`dev-only-auth-secret-change-me`).
- README and deployment docs still reference Clerk-oriented variables and setup, while the app now uses Auth.js.
- Only one generic example env file is present; no explicit production/UAT/test environment matrix or variable ownership guide was found.
### Assessment
**Partially Ready**. Basic env documentation exists, but production-grade validation and documentation are incomplete.
## Database Readiness
### Strengths
- The repository contains `20` migration files plus Drizzle metadata journal history.
- The schema defines many useful unique indexes and constraints.
- Soft-delete and active/inactive patterns are present for key domains such as organizations and permission templates.
- Transaction usage exists in several write-sensitive paths.
### Findings
- No verified migration-status check against a real target database was performed in this review.
- No documented production migration runbook or rollback runbook was found.
- No documented backup frequency, restore procedure, RPO, or RTO was found.
- Phase 5 already identified at least one important integrity gap still enforced at app level instead of DB level: active user permission template assignment uniqueness.
- `src/lib/db.ts` does not fail fast on an empty `DATABASE_URL`; it passes `""` into the postgres client constructor and relies on downstream behavior.
### Assessment
**Partially Ready** for schema-managed deployment, **Not Ready** for production database operations discipline.
## Deployment Readiness
### Strengths
- Both `Dockerfile` and `Dockerfile.bun` exist.
- Both Dockerfiles use multi-stage builds and non-root runtime users.
- Standalone Next.js output mode is wired through `BUILD_STANDALONE=true`.
- `docker-compose.yml` provides a local PostgreSQL service with a volume.
### Findings
- Dockerfiles and README deployment instructions still reference Clerk build args and sign-in/sign-up URLs, which are stale for the current Auth.js architecture.
- `docker-compose.yml` health check uses `pg_isready -d mydatabase` while the configured database name is `training_system`, so the compose health check is incorrect.
- No application health endpoint or readiness endpoint was found in `src/app/api`.
- No reverse proxy, TLS, ingress, or persistent app-file-storage deployment guidance was found.
- The app still writes protected uploads to local disk under `public/uploads/...`, which is not a robust production storage strategy and conflicts with multi-instance/container scaling.
### Assessment
**Partially Ready** for local/containerized development, **Not Ready** for production deployment standardization.
## CI/CD Readiness
### Findings
- `.github` contains only `FUNDING.yml`.
- No GitHub Actions workflows, PR checks, deployment approvals, security scans, or rollback automation were found in the repository.
- No evidence of branch protection requirements, environment protection rules, or release promotion workflow was found locally.
### Assessment
**Not Ready**. CI/CD readiness is one of the clearest production gaps.
## Logging Readiness
### Strengths
- Audit logging is implemented broadly for important business actions.
- Sentry is integrated for global error capture and request-error capture.
### Findings
- Application logging is mostly `console.error` / `console.warn`, not structured logging.
- No request ID or correlation ID propagation was found.
- No documented log retention, log sinks, or redaction policy was found.
- Sentry is configured with `sendDefaultPii: true` and `tracesSampleRate: 1`, which may be acceptable temporarily but needs explicit production policy decisions.
### Assessment
**Partially Ready** for basic debugging, **Not Ready** for mature production observability.
## Monitoring & Alerting Readiness
### Strengths
- Sentry server and client instrumentation are present.
- `app/global-error.tsx` and overview error boundaries report exceptions to Sentry.
### Findings
- No health endpoint was found for uptime probes.
- No metrics stack, alert channel configuration, uptime monitor definition, or error-budget/SLO documentation was found.
- No documented monitoring for database connection health, disk usage, upload storage growth, CPU, or memory was found.
### Assessment
**Partially Ready** for exception capture, **Not Ready** for operational monitoring.
## Backup & Recovery Readiness
### Findings
- No backup documentation was found for database or uploaded files.
- No restore procedure, restore test evidence, retention policy, encryption policy, RPO, or RTO was found.
- No disaster-recovery or incident recovery runbook was found.
### Assessment
**Not Ready**.
## Operational Workflow Readiness
### Strengths
- Seed scripts exist for master data, super admin, and UAT data.
- There is meaningful domain documentation around UAT flows and import scenarios.
### Findings
- No documented runbook was found for:
- failed imports
- notification failures
- file storage exhaustion
- user provisioning operations
- permission correction under production incidents
- emergency rollback
- Several privileged behaviors, such as organization-switch side effects, remain implementation-defined rather than operationally documented.
### Assessment
**Partially Ready** for controlled internal support by developers, **Not Ready** for formal production operations.
## Testing Readiness
### Findings
- No unit/integration/API/end-to-end test suite files were found in the repository.
- `npm test` fails because no `test` script exists.
- Existing readiness evidence is largely manual and audit-driven:
- UAT docs
- seed-data docs
- pre-UAT review
- hardening review
- Browser smoke testing is referenced in docs, but not fully automated.
### Assessment
**Not Ready** for production confidence at scale.
## Documentation Readiness
### Strengths
- The project has substantial internal documentation for architecture, UAT, reviews, and implementation history.
- `docs/PROJECT_ARCHITECTURE.md` and `docs/AI_DEVELOPMENT_GUIDE.md` are useful engineering references.
### Findings
- Root `README.md` is heavily outdated and still describes the starter template, Clerk auth, billing/workspaces demo features, and generic SaaS positioning rather than the current TMS product.
- Docker/README deployment instructions still reference Clerk-specific env vars and flows that no longer match the app.
- No production deployment guide, backup guide, restore guide, or troubleshooting runbook was found.
- No formal admin operations manual or release checklist was found.
### Assessment
**Partially Ready** for engineering context, **Not Ready** for production handoff documentation.
## Production Checklist
### Ready
- Production build compiles successfully.
- TypeScript compile passes.
- Basic env example file exists.
- Drizzle migrations exist in source control.
- Docker artifacts exist.
- Sentry instrumentation exists.
### Partially Ready
- Linting
- Passes with warnings, not clean.
- Environment configuration
- Documented minimally, but not centrally validated.
- Database schema management
- Migrations exist, but production migration/rollback procedure is undocumented.
- Deployment packaging
- Dockerfiles exist, but docs/config still have starter drift and no health endpoint.
- Logging/monitoring
- Sentry and audit logs exist, but no structured logging or alerting model is documented.
- Operational workflow support
- Some UAT/support docs exist, but no production runbooks.
### Not Ready
- Automated tests
- CI/CD pipeline
- Health/readiness checks
- Backup and recovery process
- Rollback runbook
- Production-aligned root documentation
- Resolution of known high-risk security issue from Phase 6
### Not Verified
- Real production start under load
- Real migration execution against target environments
- Real backup/restore drills
- Actual hosting/network/TLS topology
- Alert routing and on-call ownership
## Go-live Risk Assessment
### Production Blockers
1. No CI/CD workflow or enforced pre-merge release gate exists.
2. No automated test suite exists, and `npm test` is not implemented.
3. No health-check endpoint exists for uptime/readiness orchestration.
4. No backup/restore/runbook documentation exists.
5. Root deployment and setup documentation is stale and mismatched to the current Auth.js-based system.
6. Phase 6 identified unresolved high-risk public file exposure for online-lesson assets.
### Must Fix Before Go-live
1. Resolve the public online-lesson asset exposure.
2. Establish at least a minimal CI gate:
- install
- lint
- typecheck
- build
3. Add at least a smoke-level automated test path or scripted release verification.
4. Add a real health/readiness endpoint and document probe expectations.
5. Produce backup, restore, and rollback procedures.
6. Rewrite root README/deployment docs to match the actual TMS architecture and auth model.
### Can Fix After Go-live
1. Eliminate remaining lint warnings.
2. Improve structured logging and correlation IDs.
3. Expand monitoring from Sentry-only to broader service metrics and alerting.
4. Clean up demo/template production surface after operational blockers are cleared.
### Operational Risk
- High. The system currently relies too heavily on manual knowledge and ad hoc developer intervention.
### Data Risk
- High. There is no documented backup/restore posture, and file storage remains local-disk oriented.
### Security Risk
- High. Phase 6 uncovered unresolved production-impacting file-access risk.
## Findings by Severity
### High
1. No CI/CD pipeline or PR gate exists in the repository.
2. No automated test suite exists, and `npm test` is missing.
3. No backup/restore/runbook evidence exists.
4. Online-lesson public asset exposure from Phase 6 remains a production blocker.
5. Deployment and root docs remain materially out of sync with the implemented Auth.js architecture.
### Medium
1. No health/readiness endpoint exists.
2. Docker compose health check is misconfigured.
3. Dockerfiles and README still reference Clerk-oriented build args and setup concepts.
4. Logging is largely unstructured and lacks request correlation.
5. Monitoring exists mainly through Sentry and not through a documented alerting model.
6. Local-disk protected upload strategy is weak for multi-instance/container production.
### Low
1. Lint warnings remain.
2. Environment validation is uneven rather than centrally enforced.
## Production Blockers
1. Fix the online-lesson file exposure identified in Phase 6.
2. Add a real CI workflow with at least `lint`, `typecheck`, and `build`.
3. Add a test strategy with executable scripts, even if initial coverage is smoke-level.
4. Add health/readiness endpoints.
5. Document backup, restore, and rollback procedures.
6. Replace stale starter-template deployment/auth documentation.
## Recommended Remediation Order
1. Security blocker cleanup
- Resolve public asset exposure before any production deployment discussion.
2. Release gate foundation
- Add CI workflow and executable test command(s).
3. Operability baseline
- Add health endpoint, startup smoke verification, and deployment docs.
4. Data safety baseline
- Define backup, restore, retention, and rollback procedure.
5. Observability hardening
- Improve logging, alerting, and request correlation.
6. Deployment/documentation cleanup
- Remove remaining Clerk/starter assumptions from README and Docker guidance.
## Go / No-Go Recommendation
Recommendation: **No-Go**
Reason:
- The app is technically deployable, but not yet operationally production-ready.
- Build readiness alone is not enough to offset the absence of CI, tests, health checks, recovery procedures, and production-accurate deployment documentation.
- The unresolved high-risk security issue from Phase 6 should independently block production rollout.
## Commands Executed and Results
- `npm run lint`
- Passed with warnings.
- `npx tsc --noEmit`
- Passed.
- `npm run build`
- Passed successfully on Next.js `16.2.6`.
- `npm test`
- Failed because no `test` script exists.
- `rg`, `Get-Content`, `git ls-files`
- Used to inspect deployment/config/docs/test/ops artifacts.
## Files Reviewed
- `README.md`
- `env.example.txt`
- `package.json`
- `.nvmrc`
- `.gitignore`
- `Dockerfile`
- `Dockerfile.bun`
- `docker-compose.yml`
- `drizzle.config.ts`
- `drizzle/meta/_journal.json`
- `src/lib/db.ts`
- `src/instrumentation.ts`
- `src/instrumentation-client.ts`
- `next.config.ts`
- `src/proxy.ts`
- `docs/AI_DEVELOPMENT_GUIDE.md`
- `docs/PROJECT_ARCHITECTURE.md`
- `docs/pre-uat-readiness-review.md`
- `.github/FUNDING.yml`
## Areas Not Verified
- This review did not execute a real container startup, reverse-proxy integration, or hosted deployment.
- This review did not inspect actual production secret values or external secret-management systems.
- This review did not verify real backup jobs, restore drills, or infrastructure monitoring dashboards.
- This review did not perform runtime load, failover, or chaos testing.

View File

@@ -0,0 +1,160 @@
# Role & Permission Management Implementation Report
วันที่อัปเดต: 2026-07-07
## 1. Summary
พัฒนา Coding Phase 1 ของระบบ `Role & Permission Management` ตามแผนใน `docs/role-permission-management-plan.md` โดยยังคง compatibility mode เดิมของระบบไว้ และยังไม่ย้าย feature อื่นไปใช้ permission-first model
สิ่งที่ทำแล้ว:
- สร้าง feature module `src/features/permission-management/`
- เพิ่มหน้า dashboard:
- `/dashboard/permissions/templates`
- `/dashboard/permissions/matrix`
- `/dashboard/permissions/users`
- `/dashboard/permissions/audit-logs`
- เพิ่ม route handlers สำหรับ permission templates, permission catalog, user permission assignments, overrides, และ permission audit logs
- เพิ่ม super-admin-only page guard สำหรับหน้า permission management
- เพิ่มเมนู `Permission Management` ใน sidebar โดยมองเห็นเฉพาะ `super_admin`
- เพิ่ม report และ TODO ที่จำเป็นโดยไม่แก้ schema/migration ตามข้อกำหนดของ phase นี้
## 2. Files Created
- `src/features/permission-management/api/types.ts`
- `src/features/permission-management/api/service.ts`
- `src/features/permission-management/api/queries.ts`
- `src/features/permission-management/api/mutations.ts`
- `src/features/permission-management/schemas/permission-template.ts`
- `src/features/permission-management/schemas/user-permission-assignment.ts`
- `src/features/permission-management/server/permission-management-catalog.ts`
- `src/features/permission-management/server/permission-audit-service.ts`
- `src/features/permission-management/server/permission-template-data.ts`
- `src/features/permission-management/server/user-permission-data.ts`
- `src/features/permission-management/server/permission-audit-data.ts`
- `src/features/permission-management/components/permission-status-badge.tsx`
- `src/features/permission-management/components/permission-template-form-sheet.tsx`
- `src/features/permission-management/components/permission-template-row-actions.tsx`
- `src/features/permission-management/components/permission-template-columns.tsx`
- `src/features/permission-management/components/permission-template-table.tsx`
- `src/features/permission-management/components/permission-template-listing.tsx`
- `src/features/permission-management/components/permission-matrix.tsx`
- `src/features/permission-management/components/effective-permission-preview.tsx`
- `src/features/permission-management/components/user-permission-sheet.tsx`
- `src/features/permission-management/components/user-permission-row-actions.tsx`
- `src/features/permission-management/components/user-permission-columns.tsx`
- `src/features/permission-management/components/user-permission-table.tsx`
- `src/features/permission-management/components/user-permission-listing.tsx`
- `src/features/permission-management/components/permission-audit-log-columns.tsx`
- `src/features/permission-management/components/permission-audit-log-table.tsx`
- `src/features/permission-management/components/permission-audit-log-listing.tsx`
- `src/app/dashboard/permissions/templates/page.tsx`
- `src/app/dashboard/permissions/matrix/page.tsx`
- `src/app/dashboard/permissions/users/page.tsx`
- `src/app/dashboard/permissions/audit-logs/page.tsx`
- `src/app/api/permissions/catalog/route.ts`
- `src/app/api/permission-templates/route.ts`
- `src/app/api/permission-templates/[id]/route.ts`
- `src/app/api/permission-templates/[id]/clone/route.ts`
- `src/app/api/permission-templates/[id]/activate/route.ts`
- `src/app/api/permission-templates/[id]/deactivate/route.ts`
- `src/app/api/user-permissions/route.ts`
- `src/app/api/user-permissions/[userId]/route.ts`
- `src/app/api/user-permissions/[userId]/assignment/route.ts`
- `src/app/api/user-permissions/[userId]/overrides/route.ts`
- `src/app/api/user-permissions/[userId]/overrides/[overrideId]/route.ts`
- `src/app/api/user-permissions/[userId]/effective/route.ts`
- `src/app/api/permission-audit-logs/route.ts`
- `docs/role-permission-management-implementation-report.md`
## 3. Files Modified
- `src/lib/auth/session.ts`
- `src/lib/auth/page-guards.ts`
- `src/config/nav-config.ts`
## 4. API Implemented
### Permission Templates
- `GET /api/permission-templates`
- `POST /api/permission-templates`
- `GET /api/permission-templates/[id]`
- `PATCH /api/permission-templates/[id]`
- `DELETE /api/permission-templates/[id]`
- `POST /api/permission-templates/[id]/clone`
- `POST /api/permission-templates/[id]/activate`
- `POST /api/permission-templates/[id]/deactivate`
### Permission Catalog
- `GET /api/permissions/catalog`
### User Permissions
- `GET /api/user-permissions`
- `GET /api/user-permissions/[userId]`
- `PUT /api/user-permissions/[userId]/assignment`
- `GET /api/user-permissions/[userId]/effective`
- `POST /api/user-permissions/[userId]/overrides`
- `PATCH /api/user-permissions/[userId]/overrides/[overrideId]`
- `DELETE /api/user-permissions/[userId]/overrides/[overrideId]`
### Permission Audit Logs
- `GET /api/permission-audit-logs`
## 5. UI Pages
- `Permission Templates`
- table + search + pagination + sorting
- create/edit sheet
- clone / activate / deactivate / soft delete actions
- `Permission Matrix`
- grouped permission catalog
- read-only checklist-style layout
- `User Permissions`
- user table + search
- manage sheet สำหรับ assign template และ allow/deny permissions
- effective permission preview
- `Permission Audit Logs`
- table + filters ผ่าน query params เดิมของระบบ
- data source ใหม่จาก `permission_audit_logs`
## 6. Remaining TODO
- เพิ่ม DB-level constraint สำหรับ `1 active template per user per organization`
- เพิ่ม index สำหรับ `permission_audit_logs` ถ้าข้อมูลโต
- ปรับ UI assignment ให้รองรับ granular override management แบบแยก create/edit/delete ในหน้าเดียว
- แสดง `target user` ใน permission audit log ให้ครบกว่านี้
- เพิ่ม form-level search/filter สำหรับ permission checklist เมื่อจำนวน permission โตขึ้น
- เชื่อม default template seed จริงในฐานข้อมูล
## 7. Known Limitations
- phase นี้ยังไม่แก้ schema หรือ migration ตาม requirement
- การบังคับ `1 active template` ตอนนี้ทำใน application logic และมี TODO ชัดเจนไว้ใน server layer
- หน้า `User Permissions` ใช้ flow แบบ save assignment ทั้งชุดในครั้งเดียวเป็นหลัก แม้ route override แยกจะถูกสร้างไว้แล้ว
- หน้า `Permission Matrix` เป็น read-only reference screen ยังไม่ใช่ editor เต็มรูปแบบ
- หน้า `Permission Audit Logs` ยังแสดงรายละเอียดแบบย่อ และยังไม่ได้ reuse UI รายการเดิมแบบ 100% ในเชิง component-level
## 8. Compatibility Status
ยังคง compatibility mode เดิมไว้ทั้งหมด:
- ไม่ลบ `businessRole`
- ไม่ลบ `isHRD()`
- ไม่ลบ `isIT()`
- ไม่ย้าย feature runtime อื่นไปใช้ permission-first ในรอบนี้
- ไม่แก้ navigation เดิม ยกเว้นเพิ่มเมนู `Permission Management`
- authorization ของหน้าและ API ใหม่ทั้งหมดใช้ `super_admin` เท่านั้น โดยไม่กระทบ flow เดิมของ HRD / IT / Employee
## Verification
ตรวจสอบแล้ว:
- `npx oxlint src/features/permission-management src/app/api/permission-templates src/app/api/user-permissions src/app/api/permissions src/app/api/permission-audit-logs src/app/dashboard/permissions src/config/nav-config.ts src/lib/auth/page-guards.ts src/lib/auth/session.ts` ผ่าน
- `npx tsc --noEmit` ผ่าน

View File

@@ -0,0 +1,625 @@
# Role & Permission Management Plan
วันที่อัปเดต: 2026-07-07
## 1. Objective
วางแผนพัฒนาหน้า Role & Permission Management สำหรับระบบ TMS โดยย้ายการจัดการสิทธิ์ไปสู่ permission-first model ที่ใช้ Permission Template เป็นแกนหลัก และยังคง compatibility กับระบบ role-based เดิมในช่วง migration
เป้าหมายของเอกสารนี้:
- กำหนดขอบเขตหน้าจัดการ Permission Template
- กำหนดขอบเขตหน้าจัดการ Permission Matrix
- กำหนดขอบเขตหน้าจัดการสิทธิ์รายผู้ใช้
- กำหนดแนวทางหน้า Audit Log สำหรับ permission changes
- สรุป schema, route, UI, และ migration work ที่ต้องทำใน coding phase ถัดไป
## 2. Current System Findings
จากการตรวจของจริงในโปรเจกต์ พบว่า authorization ตอนนี้เป็น hybrid model:
- session เริ่มรองรับ `effectivePermissions` แล้วใน [src/auth.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/auth.ts:254)
- มี permission helper ใหม่ใน [src/lib/auth/authorization.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/authorization.ts:27) และ [src/lib/auth/session.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/session.ts:215)
- แต่ navigation และหลาย feature ยังพึ่ง `businessRole`, `isHRD()`, `isIT()` อยู่ เช่น [src/config/nav-config.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts:23) และ [src/hooks/use-nav.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/hooks/use-nav.ts:53)
- page guards หลักยังเป็น `requireHRD()`, `requireIT()`, `requireUserManagementAccess()` ใน [src/lib/auth/page-guards.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/page-guards.ts:20)
- user management ปัจจุบันเปิดผ่าน `requireUserManagementAccess()` และยังผูกกับ logic IT/business role ที่ [src/app/api/users/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/users/route.ts:23)
- audit log ปัจจุบันมีระบบกลางอยู่แล้วใน [src/features/audit-logs/server/audit-service.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/audit-logs/server/audit-service.ts:5) แต่ยังไม่มี action/module สำหรับ permission management โดยเฉพาะ
สรุปสั้น ๆ:
- foundation ฝั่ง permission model เริ่มมีแล้ว
- management UI ยังไม่มี
- route/API สำหรับ permission template และ user assignment ยังไม่มี
- nav และ feature access หลายส่วนยังไม่ย้ายตาม model ใหม่
## 3. Existing Files / Modules Related to Role & Permission
### Auth / Session / RBAC
- [src/auth.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/auth.ts:1)
- [src/lib/auth/roles.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/roles.ts:1)
- [src/lib/auth/permissions.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/permissions.ts:1)
- [src/lib/auth/authorization.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/authorization.ts:1)
- [src/lib/auth/session.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/session.ts:1)
- [src/lib/auth/page-guards.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/page-guards.ts:1)
- [src/types/next-auth.d.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/next-auth.d.ts:1)
- [src/types/index.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/index.ts:1)
### Navigation
- [src/config/nav-config.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts:1)
- [src/hooks/use-nav.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/hooks/use-nav.ts:1)
### Database / Schema
- [src/db/schema.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/db/schema.ts:104)
- [drizzle/0015_permission_template_authorization.sql](/D:/WY-2569/HRD/training-system-minimal-refactor/drizzle/0015_permission_template_authorization.sql:1)
ตารางที่เกี่ยวข้อง:
- `users`
- `organizations`
- `memberships`
- `permission_templates`
- `permission_template_permissions`
- `user_permission_template_assignments`
- `user_permission_overrides`
- `permission_audit_logs`
- `audit_logs`
### Existing Pages / Features ที่ควรเป็นต้นแบบ
- Users listing page: [src/app/dashboard/users/page.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/users/page.tsx:1)
- Audit log page: [src/app/dashboard/audit-logs/page.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/audit-logs/page.tsx:1)
- Audit log feature: [src/features/audit-logs/components/audit-log-listing.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/audit-logs/components/audit-log-listing.tsx)
- Users feature: [src/features/users/components/user-listing.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/users/components/user-listing.tsx)
### Shared UI / Form / Table Components
- `DataTable` stack in `src/components/ui/table/*`
- `Dialog` in [src/components/ui/dialog.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/components/ui/dialog.tsx)
- `Sheet` in [src/components/ui/sheet.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/components/ui/sheet.tsx)
- `AlertDialog` in [src/components/ui/alert-dialog.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/components/ui/alert-dialog.tsx)
- `Button` in [src/components/ui/button.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/components/ui/button.tsx)
- `Badge` in [src/components/ui/badge.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/components/ui/badge.tsx)
- TanStack Form wrapper in [src/components/ui/tanstack-form.tsx](/D:/WY-2569/HRD/training-system-minimal-refactor/src/components/ui/tanstack-form.tsx)
## 4. Proposed Permission Model
หลักการเป้าหมาย:
```text
Organization
-> Permission Template
-> Template Permissions
-> User Assignment
-> User Overrides
-> Effective Permissions
```
กติกาที่ต้องใช้กับหน้า management:
- ผู้ใช้ 1 คนต่อ 1 organization ต้องมี active template ได้เพียง 1 template
- ผู้ใช้สามารถมี extra permission รายบุคคลได้
- ผู้ใช้สามารถมี deny override ได้
- effective permissions ต้องคำนวณจาก:
- template permissions
- allow overrides
- deny overrides
- ignore expired overrides
- `systemRole = super_admin` ยังเป็น system-level override ชั่วคราว
ข้อสังเกตจาก schema ปัจจุบัน:
- requirement บอกว่า 1 user มีได้ 1 template
- แต่ [src/db/schema.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/db/schema.ts:200) ตอนนี้ยังอนุญาตหลาย assignment ได้ ตราบใดที่ `template_id` ต่างกัน
- coding phase ถัดไปควรเปลี่ยนเป็น constraint ระดับ `(organization_id, user_id)` สำหรับ active assignment เดียว หรือใช้ partial unique index ถ้าระบบยังต้องเก็บ history
## 5. Proposed Pages
### 5.1 Permission Template Management
เส้นทางที่เสนอ:
- `/dashboard/permissions/templates`
หน้าที่:
- แสดงรายการ template ทั้งหมดของ active organization
- แสดงสถานะ `system`, `default`, `active`, `inactive`
- แสดงจำนวนผู้ใช้ที่ถูก assign อยู่
- เปิด dialog/sheet สำหรับ create
- เปิด detail/edit page หรือ sheet สำหรับแก้ไข permissions
- clone template
- deactivate template
- soft delete template ที่ไม่ใช่ system default
ฟิลด์สำคัญที่ต้องแสดง:
- `name`
- `code`
- `description`
- `isSystem`
- `isDefault`
- `isActive`
- `assignedUserCount`
- `updatedAt`
- `updatedBy`
ข้อกำหนด:
- ห้าม delete hard-delete
- ห้าม deactivate หรือ delete template ระบบ ถ้าจะกันเด็ดขาดให้ทำที่ server
- จำกัดสิทธิ์เฉพาะ super admin
### 5.2 Permission Matrix Page
เส้นทางที่เสนอ:
- `/dashboard/permissions/matrix`
หน้าที่:
- แสดง permission catalog แบบ grouped by module
- แสดง matrix module x action
- ใช้เป็น reference screen และ editor input สำหรับ template form
กลุ่ม module เริ่มต้น:
- Dashboard
- Training Records
- Online Lessons
- Announcements
- Employees
- Courses
- Training Matrix
- Reports
- Audit Logs
- Organizations
- Permission Management
- Notifications
action groups เริ่มต้น:
- View
- Create
- Edit
- Delete
- Submit
- Approve
- Reject
- Publish
- Archive
- Export
- Manage
ข้อเสนอ UI:
- แสดง grouped card/list ด้านซ้าย
- ด้านขวาเป็น checklist permissions
- รองรับ search permission
- แสดง code จริง เช่น `training_record:approve`
### 5.3 User Permission Assignment
เส้นทางที่เสนอ:
- `/dashboard/permissions/users`
หน้าที่:
- ค้นหา user
- แสดง employee code, name, department, position
- assign template ได้ 1 รายการ
- เพิ่ม extra permission รายบุคคล
- deny permission รายบุคคล
- ลบ override
- preview final effective permissions
- แสดง template ปัจจุบัน
- แสดงประวัติการเปลี่ยนล่าสุด
ข้อกำหนด:
- ใช้ active organization เป็น scope เสมอ
- การบันทึกต้องทำแบบ transactional
- จำกัดสิทธิ์เฉพาะ super admin
### 5.4 Permission Audit Log
เส้นทางที่เสนอ:
- `/dashboard/permissions/audit-logs`
หน้าที่:
- ค้นหาประวัติการเปลี่ยน permission
- filter ตาม actor, target user, template, action, date range
- ดู before/after payload
- ดูเหตุผลการเปลี่ยน
หมายเหตุ:
- สามารถ reuse audit log table pattern เดิมจาก audit-logs feature ได้
- แต่ data source ควรอ่านจาก `permission_audit_logs` โดยตรง หรือทำ unified view ในภายหลัง
## 6. Proposed Database / Schema Changes
รอบ implement ถัดไปควรแก้ schema เพิ่มจากของปัจจุบันดังนี้:
1. Tighten assignment constraint
- เปลี่ยน `user_permission_template_assignments` ให้รองรับ active assignment เดียวต่อ `(organization_id, user_id)`
- ถ้าต้องเก็บ history ให้ใช้ `is_active` + partial unique index
2. Assignment history strategy
- ถ้าต้องเปลี่ยน template ให้ deactivate row เดิมก่อน แล้วสร้าง row ใหม่
- หลีกเลี่ยง update ทับเพื่อไม่เสีย audit trail
3. Override semantics
- คง `effect = allow|deny`
- ใช้ `is_active`, `expires_at`, `revoked_at`, `revoked_by`
4. Permission template lifecycle
- คง `deleted_at` สำหรับ soft delete
- ใช้ `is_active` สำหรับ deactivate
5. Permission audit detail
- พิจารณาเพิ่ม `organization_id + action + created_at` indexes ถ้าข้อมูลโต
- พิจารณาเพิ่ม `target_template_code` หรือ `target_template_name_snapshot` ถ้าต้องการอ่าน log ย้อนหลังง่ายขึ้น
## 7. Proposed API Routes
### Permission Templates
- `GET /api/permission-templates`
- `POST /api/permission-templates`
- `GET /api/permission-templates/[id]`
- `PATCH /api/permission-templates/[id]`
- `POST /api/permission-templates/[id]/clone`
- `POST /api/permission-templates/[id]/deactivate`
- `POST /api/permission-templates/[id]/activate`
- `DELETE /api/permission-templates/[id]`
### Permission Matrix / Catalog
- `GET /api/permissions/catalog`
### User Permission Assignments
- `GET /api/user-permissions`
- `GET /api/user-permissions/[userId]`
- `PUT /api/user-permissions/[userId]/assignment`
- `POST /api/user-permissions/[userId]/overrides`
- `PATCH /api/user-permissions/[userId]/overrides/[overrideId]`
- `DELETE /api/user-permissions/[userId]/overrides/[overrideId]`
- `GET /api/user-permissions/[userId]/effective`
### Permission Audit Logs
- `GET /api/permission-audit-logs`
ทุก route ต้อง:
- ใช้ `requireSession()` + super-admin gate
- scope ด้วย active organization
- validate payload ด้วย Zod
- เขียน audit log ทุกครั้งที่มีการเปลี่ยน template/assignment/override
## 8. Proposed UI Components
### Feature module ที่เสนอ
```text
src/features/permission-management/
api/
types.ts
service.ts
queries.ts
mutations.ts
components/
permission-template-listing.tsx
permission-template-table.tsx
permission-template-columns.tsx
permission-template-form-sheet.tsx
permission-matrix.tsx
user-permission-assignment-listing.tsx
user-permission-assignment-form.tsx
permission-effective-preview.tsx
permission-audit-log-listing.tsx
schemas/
permission-template.ts
user-permission-assignment.ts
server/
permission-template-data.ts
user-permission-data.ts
permission-audit-data.ts
```
### Shared components ที่ควร reuse
- `DataTable` สำหรับ template list และ permission audit log
- `Sheet` หรือ `Dialog` สำหรับ create/edit template
- `AlertDialog` สำหรับ deactivate / soft delete confirmation
- `Badge` สำหรับ status เช่น `System`, `Default`, `Active`, `Inactive`
- `useAppForm` ผ่าน `tanstack-form.tsx`
### UI decisions ที่แนะนำ
- Template list ใช้ table
- Template edit ใช้ sheet เพราะมี permission checklist ยาว
- Permission matrix ใช้ accordion/group sections
- User assignment ใช้ split layout:
- ซ้ายเป็น user summary + template selector
- ขวาเป็น extra/deny overrides + effective preview
## 9. Access Control Rules
กติกาเป้าหมายของหน้าชุดนี้:
- Super Admin เท่านั้นที่เข้าหน้า permission management ได้
- Admin, HRD, Employee ห้ามเข้าทั้ง page และ API
- nav item ต้องซ่อนสำหรับ non-super-admin
- route handler ต้อง enforce ซ้ำเสมอ
กติกา permission ที่เสนอ:
- `permission_template:read`
- `permission_template:create`
- `permission_template:update`
- `permission_template:clone`
- `permission_template:delete`
- `permission_template:assign`
- `user_permission:read`
- `user_permission:update`
- `user_permission:override`
- `user_permission:remove_override`
ข้อเสนอสำหรับ phase implement:
- ระยะสั้นให้ super admin gate เป็นหลัก
- ระยะถัดไปค่อย map ว่า super admin ต้องถือ permission เหล่านี้ใน template ด้วยหรือไม่
## 10. Audit Log Strategy
ของเดิม:
- มี `audit_logs` และ `logAuditEvent()` อยู่แล้ว
- มี audit log UI/read model อยู่แล้วใน feature `audit-logs`
สิ่งที่ควรทำสำหรับ permission management:
- ใช้ `permission_audit_logs` เป็น source หลักของ domain นี้
- เพิ่ม action catalog เช่น:
- `TEMPLATE_CREATE`
- `TEMPLATE_UPDATE`
- `TEMPLATE_CLONE`
- `TEMPLATE_DEACTIVATE`
- `TEMPLATE_ACTIVATE`
- `TEMPLATE_SOFT_DELETE`
- `USER_TEMPLATE_ASSIGN`
- `USER_TEMPLATE_CHANGE`
- `USER_OVERRIDE_ALLOW`
- `USER_OVERRIDE_DENY`
- `USER_OVERRIDE_REMOVE`
payload ที่ควรเก็บ:
- `organizationId`
- `actorUserId`
- `targetUserId`
- `templateId`
- `assignmentId`
- `overrideId`
- `action`
- `reason`
- `before`
- `after`
- `createdAt`
ข้อเสนอ implementation:
- เขียน helper ใหม่เฉพาะ permission domain เช่น `logPermissionAuditEvent()`
- ภายในอาจ reuse pattern จาก `logAuditEvent()`
## 11. Migration Strategy from Role-based to Permission-first
ระยะ migration ควรแบ่งเป็น 2 track:
### Track A: Management surfaces
- สร้าง feature permission management ให้เสร็จก่อน
- เปิดใช้เฉพาะ super admin
- เริ่ม assign template จริงให้ผู้ใช้
### Track B: Runtime authorization cleanup
- ค่อย ๆ เปลี่ยน nav, page guards, และ feature checks จาก `businessRole` ไปเป็น permission
- เริ่มจาก features สำคัญ:
- users
- audit logs
- announcements
- online lessons
- training records
compatibility rules:
- ยังไม่ลบ `businessRole`
- ยังไม่ลบ `isHRD()`, `isIT()`
- session ยัง fallback ไป `memberships.permissions` หรือ default role permissions ได้
- feature ใหม่ห้ามสร้างบน role-based gate
## 12. Implementation Phases
### Phase 1: Planning and contracts
- เอกสารแผนนี้
- ยืนยัน route names, page names, permission groups
### Phase 2: Schema tightening
- ปรับ assignment constraint ให้เหลือ 1 active template ต่อ user/org
- เพิ่ม indexes ที่จำเป็น
- เตรียม seed strategy สำหรับ default templates
### Phase 3: Server-side domain
- สร้าง feature `permission-management`
- เพิ่ม server helpers สำหรับ template CRUD, assignment, override, effective preview
- เพิ่ม audit log helper ของ permission domain
### Phase 4: API routes
- เพิ่ม route handlers ทั้งหมดสำหรับ template, assignment, audit
- ผูก super-admin-only access
### Phase 5: UI pages
- Template management page
- Matrix page
- User assignment page
- Permission audit log page
### Phase 6: Navigation and guards
- เพิ่ม nav item ใหม่สำหรับ permission management
- ผูกกับ permission หรือ super-admin gate
### Phase 7: Runtime migration follow-up
- ค่อย ๆ ย้าย feature gates ที่ยังใช้ `businessRole`
## 13. Validation & Testing Checklist
### Access
- Super admin เข้าหน้า permission management ได้
- non-super-admin เข้า page ไม่ได้
- non-super-admin เรียก API ไม่ได้
### Templates
- สร้าง template ได้
- clone template ได้
- แก้ไข template ได้
- deactivate template ได้
- soft delete template ได้
- system template ถูกป้องกันการลบ
### Assignments
- user 1 คน assign active template ได้เพียง 1 template ต่อ organization
- เปลี่ยน template แล้วของเดิมถูก deactivate
- preview effective permissions ถูกต้อง
### Overrides
- allow override ทำงานถูกต้อง
- deny override override สิทธิ์จาก template ได้
- remove override ได้
- expired override ไม่ถูกนับ
### Audit
- ทุก action สำคัญมี permission audit log
- before/after payload อ่านได้
- filter log ได้ตาม actor/target/action/date
### Compatibility
- ผู้ใช้เดิมที่ยังไม่มี template ยังใช้งานระบบได้ผ่าน fallback
- หน้าเดิมไม่พังระหว่าง migration
## 14. Risks / Edge Cases
1. Current schema กับ requirement ยังไม่ตรงกัน
ตอนนี้ assignment table ยังเปิดโอกาสให้หลาย template active พร้อมกันได้ ถ้าไม่ tighten constraint ก่อน UI จะทำให้ data model สับสน
2. Hybrid authorization ช่วงเปลี่ยนผ่าน
บาง feature ใช้ `effectivePermissions` แล้ว แต่ nav/page/API จำนวนมากยังพึ่ง `businessRole`
3. Multi-organization scope
ทุกหน้า permission management ต้องใช้ active organization ชัดเจน ไม่เช่นนั้นอาจ assign template ข้ามองค์กรผิดได้
4. Super admin without membership
ระบบปัจจุบันยังมี flow ที่ super admin สามารถเข้าถึงหลายองค์กรได้แม้ไม่มี membership แบบปกติ จึงต้องระวัง context ตอน create/update permission data
5. Template deactivation edge case
ถ้า deactivate template ที่ยังมีผู้ใช้ active อยู่ ต้องกำหนดกติกาชัดว่า:
- ห้าม deactivate
- หรืออนุญาต แต่ต้องเตือนว่าผู้ใช้จะ fallback ไปสิทธิ์เดิม
ข้อเสนอคือห้าม deactivate ถ้ายังมี active assignments อยู่ เว้นแต่เป็น explicit admin action พร้อม reason
6. Permission catalog drift
ถ้ามีการเพิ่ม permission ใหม่ใน `permissions.ts` แต่ไม่อัปเดต matrix/UI/template seeds จะทำให้ template ไม่ครบ
## 15. Files Expected to be Created or Modified
### New docs
- `docs/role-permission-management-plan.md`
### New pages
- `src/app/dashboard/permissions/templates/page.tsx`
- `src/app/dashboard/permissions/matrix/page.tsx`
- `src/app/dashboard/permissions/users/page.tsx`
- `src/app/dashboard/permissions/audit-logs/page.tsx`
### New API routes
- `src/app/api/permission-templates/route.ts`
- `src/app/api/permission-templates/[id]/route.ts`
- `src/app/api/permission-templates/[id]/clone/route.ts`
- `src/app/api/permission-templates/[id]/activate/route.ts`
- `src/app/api/permission-templates/[id]/deactivate/route.ts`
- `src/app/api/user-permissions/route.ts`
- `src/app/api/user-permissions/[userId]/route.ts`
- `src/app/api/user-permissions/[userId]/assignment/route.ts`
- `src/app/api/user-permissions/[userId]/overrides/route.ts`
- `src/app/api/user-permissions/[userId]/overrides/[overrideId]/route.ts`
- `src/app/api/user-permissions/[userId]/effective/route.ts`
- `src/app/api/permission-audit-logs/route.ts`
- `src/app/api/permissions/catalog/route.ts`
### New feature module
- `src/features/permission-management/**`
### Existing files likely to be updated
- `src/config/nav-config.ts`
- `src/hooks/use-nav.ts`
- `src/lib/auth/page-guards.ts`
- `src/lib/auth/session.ts`
- `src/lib/auth/permissions.ts`
- `src/lib/auth/authorization.ts`
- `src/features/audit-logs/server/audit-service.ts`
- `src/types/index.ts`
### Schema and migration files for later phase
- `src/db/schema.ts`
- `drizzle/*.sql`
## Recommendation
ถ้าจะเริ่ม coding phase จากแผนนี้ จุดเริ่มที่ปลอดภัยที่สุดคือ:
1. Tighten schema ให้ user มี active template เดียวต่อ organization
2. ทำ server layer ของ permission-management ก่อน UI
3. เปิดหน้า Template Management ก่อนหน้า User Assignment
4. ค่อยเพิ่ม Permission Audit Log page เป็นลำดับถัดไป
แนวทางนี้จะทำให้ model แน่นก่อน แล้ว UI จะไม่ต้องย้อนแก้ภายหลัง

View File

@@ -0,0 +1,264 @@
# Role Permission Matrix
วันที่จัดทำ: 2026-07-07
## Summary
เอกสารนี้กำหนดโครงสร้าง role และ permission model ใหม่สำหรับระบบ Training Management System เพื่อใช้แทนแนวทางเดิมที่พึ่งพา:
- `systemRole`
- `membership.role`
- `businessRole`
แบบกระจายหลายชั้นและ hard-code ตาม feature
เป้าหมายของโมเดลใหม่คือ:
- แยก `system-level access` ออกจาก `organization-level access`
- ใช้ `permissions` เป็นตัวตัดสินสิทธิ์จริงที่ระดับ API/server
- รองรับ workflow ที่ซับซ้อนขึ้น เช่น approve, publish, schedule, archive, revision
- ลดการพึ่งพา `businessRole` ให้เหลือ compatibility layer ชั่วคราว
## Access Model
ระบบใหม่ควรมี 3 ชั้น:
| Layer | Purpose | Example |
| --- | --- | --- |
| `system_role` | สิทธิ์ระดับ platform | `super_admin`, `support`, `standard` |
| `organization_role` | บทบาทหลักในแต่ละองค์กร | `owner`, `org_admin`, `hrd_manager`, `hrd_staff`, `employee` |
| `permissions[]` | สิทธิ์ละเอียดระดับ feature/action | `announcement:publish`, `users:manage` |
## System Roles
| System Role | Meaning |
| --- | --- |
| `super_admin` | เข้าถึงทุกองค์กรและทุกฟังก์ชันได้ |
| `support` | เข้าถึงเพื่อ support หรือ diagnose ได้ตาม policy ที่กำหนด |
| `standard` | ใช้สิทธิ์ตาม role ในองค์กรเป็นหลัก |
## Organization Roles
| Organization Role | Meaning |
| --- | --- |
| `owner` | เจ้าขององค์กร จัดการทุกอย่างในองค์กร |
| `org_admin` | ผู้ดูแลองค์กร จัดการ users, master data, reports, workflow ได้ |
| `hrd_manager` | อนุมัติ, publish, schedule, archive workflow ฝั่ง HRD ได้ |
| `hrd_staff` | สร้าง, แก้ draft, submit ได้ แต่อนุมัติและ publish ไม่ได้ |
| `employee` | ผู้ใช้ทั่วไป เห็นเฉพาะข้อมูล public และข้อมูลของตัวเอง |
## Permission Catalog
### Platform / Organization
| Permission | Meaning |
| --- | --- |
| `organization:manage` | จัดการข้อมูลองค์กร |
| `organization:switch` | สลับ active organization |
| `membership:manage` | จัดการสมาชิกในองค์กร |
| `roles:assign` | เปลี่ยน role ให้สมาชิก |
| `permissions:assign` | กำหนด permission รายคนหรือราย role |
### Users / Employee Directory
| Permission | Meaning |
| --- | --- |
| `users:read` | ดูรายการผู้ใช้ |
| `users:create` | สร้างผู้ใช้ |
| `users:update` | แก้ผู้ใช้ |
| `users:delete` | ลบหรือปิดผู้ใช้ |
| `users:manage` | จัดการผู้ใช้ทั้งหมด |
| `employee_directory:read` | ดู employee directory |
| `employee_directory:write` | แก้ employee directory |
| `employee_import:run` | import พนักงาน |
| `employee_master_review:approve` | อนุมัติ master data จาก import |
### Courses / Training Policy / Training Matrix
| Permission | Meaning |
| --- | --- |
| `courses:read` | ดูหลักสูตร |
| `courses:write` | จัดการหลักสูตร |
| `training_policy:read` | ดูนโยบาย |
| `training_policy:write` | จัดการนโยบาย |
| `training_matrix:read` | ดู training matrix |
| `training_matrix:write` | จัดการ training matrix |
### Training Records
| Permission | Meaning |
| --- | --- |
| `training_record:self_read` | ดูประวัติอบรมของตัวเอง |
| `training_record:self_write` | สร้างหรือแก้ draft ของตัวเอง |
| `training_record:read_all` | ดูข้อมูลทั้งองค์กร |
| `training_record:write_all` | สร้างหรือแก้แทนคนอื่นในองค์กร |
| `training_record:submit` | ส่งตรวจสอบ |
| `training_record:review` | อนุมัติหรือขอแก้ไข |
| `training_record:delete_draft` | ลบ draft |
| `training_record:manage_attachments` | จัดการไฟล์แนบ |
### Announcements
| Permission | Meaning |
| --- | --- |
| `announcement:read_public` | ดูประกาศที่เผยแพร่แล้ว |
| `announcement:read_all` | ดูประกาศทุกสถานะ |
| `announcement:create` | สร้างประกาศ |
| `announcement:edit_draft` | แก้ draft หรือ revision |
| `announcement:delete_draft` | ลบ draft |
| `announcement:submit` | ส่งอนุมัติ |
| `announcement:approve` | อนุมัติ |
| `announcement:reject` | ตีกลับ |
| `announcement:publish` | เผยแพร่ทันที |
| `announcement:schedule` | ตั้งเวลาเผยแพร่ |
| `announcement:unpublish` | ยกเลิกการเผยแพร่ |
| `announcement:archive` | เก็บถาวร |
| `announcement:revision` | สร้าง draft revision จาก version ที่เผยแพร่แล้ว |
| `announcement:preview` | preview ได้ |
### Online Lessons
| Permission | Meaning |
| --- | --- |
| `online_lesson:read_public` | ดูบทเรียนที่เผยแพร่แล้ว |
| `online_lesson:read_all` | ดูบทเรียนทุกสถานะ |
| `online_lesson:create` | สร้างบทเรียน |
| `online_lesson:edit_draft` | แก้ draft หรือ revision |
| `online_lesson:delete_draft` | ลบ draft |
| `online_lesson:submit` | ส่งอนุมัติ |
| `online_lesson:approve` | อนุมัติ |
| `online_lesson:reject` | ตีกลับ |
| `online_lesson:publish` | เผยแพร่ทันที |
| `online_lesson:schedule` | ตั้งเวลาเผยแพร่ |
| `online_lesson:unpublish` | ยกเลิกการเผยแพร่ |
| `online_lesson:archive` | เก็บถาวร |
| `online_lesson:revision` | สร้าง draft revision |
| `online_lesson:preview` | preview ได้ |
### Reports / Audit / Notifications
| Permission | Meaning |
| --- | --- |
| `reports:self_read` | ดูรายงานของตัวเอง |
| `reports:organization_read` | ดูรายงานทั้งองค์กร |
| `reports:export` | export รายงาน |
| `audit_logs:read` | ดู audit logs |
| `notifications:read` | ดู notifications |
| `notifications:manage` | จัดการ notification flows หรือ templates |
## Recommended Role-Permission Matrix
| Permission Group | owner | org_admin | hrd_manager | hrd_staff | employee |
| --- | --- | --- | --- | --- | --- |
| `organization:*` | Y | Y | N | N | N |
| `membership:manage` | Y | Y | N | N | N |
| `roles:assign` | Y | Y | N | N | N |
| `permissions:assign` | Y | Y | N | N | N |
| `users:*` | Y | Y | N | N | N |
| `employee_directory:read` | Y | Y | Y | Y | N |
| `employee_directory:write` | Y | Y | Y | Y | N |
| `employee_import:run` | Y | Y | Y | N | N |
| `employee_master_review:approve` | Y | Y | Y | N | N |
| `courses:read` | Y | Y | Y | Y | N |
| `courses:write` | Y | Y | Y | Y | N |
| `training_policy:read` | Y | Y | Y | N | N |
| `training_policy:write` | Y | Y | Y | N | N |
| `training_matrix:read` | Y | Y | Y | Y | N |
| `training_matrix:write` | Y | Y | Y | Y | N |
| `training_record:self_*` | Y | Y | Y | Y | Y |
| `training_record:read_all` | Y | Y | Y | Y | N |
| `training_record:write_all` | Y | Y | Y | Y | N |
| `training_record:submit` | Y | Y | Y | Y | Y |
| `training_record:review` | Y | Y | Y | N | N |
| `announcement:create/edit_draft/delete_draft/submit/preview` | Y | Y | Y | Y | N |
| `announcement:approve/reject/publish/schedule/unpublish/archive/revision` | Y | Y | Y | N | N |
| `online_lesson:create/edit_draft/delete_draft/submit/preview` | Y | Y | Y | Y | N |
| `online_lesson:approve/reject/publish/schedule/unpublish/archive/revision` | Y | Y | Y | N | N |
| `reports:self_read` | Y | Y | Y | Y | Y |
| `reports:organization_read` | Y | Y | Y | N | N |
| `reports:export` | Y | Y | Y | N | N |
| `audit_logs:read` | Y | Y | N | N | N |
| `notifications:read` | Y | Y | Y | Y | Y |
| `notifications:manage` | Y | Y | Y | N | N |
## Page Access Matrix
| Page / Module | owner | org_admin | hrd_manager | hrd_staff | employee |
| --- | --- | --- | --- | --- | --- |
| Dashboard Overview | Y | Y | Y | Y | Y |
| Notifications | Y | Y | Y | Y | Y |
| Training Records | Y | Y | Y | Y | Y |
| Pending Review | Y | Y | Y | N | N |
| Announcements | Y | Y | Y | Y | Y, public-only content |
| Online Lessons | Y | Y | Y | Y | Y, public-only content |
| Users | Y | Y | N | N | N |
| Employee Directory | Y | Y | Y | Y | N |
| Courses | Y | Y | Y | Y | N |
| Training Policy | Y | Y | Y | N | N |
| Training Matrix | Y | Y | Y | Y | N |
| Reports | Y | Y | Y | N | Y, self-only |
| Audit Logs | Y | Y | N | N | N |
## Special Rules
### Employee visibility
Employee-facing queries ต้องถูกจำกัดที่ server-side เสมอ:
- content ต้องใช้ `status = published`
- versioned content ต้องใช้ `is_current = true`
- training records ต้อง self-scope หรือผ่าน employee mapping ที่อนุญาต
### HRD staff
HRD staff ต้อง:
- สร้าง content ได้
- แก้ draft ได้
- submit ได้
แต่ต้องไม่มีสิทธิ์:
- approve
- publish
- schedule publish
- unpublish
- archive
### HRD manager
HRD manager ต้อง:
- approve / reject ได้
- publish ได้
- schedule ได้
- archive ได้
- preview ได้
### Super admin
`super_admin` เป็น system override:
- bypass organization-level permission checks ได้
- แต่ควรยังใช้ organization context เดิมในการ query/write เพื่อให้ audit trail ชัดเจน
## Design Notes For Current Repo
สำหรับ repo ปัจจุบัน:
- `systemRole` ยังควรเก็บไว้
- `memberships.role` ควรถูกย้ายจาก `owner/admin/member` ไปสู่ role model ใหม่ หรือเพิ่ม layer mapping ใหม่
- `businessRole` ควรถูกลดบทบาทเหลือ compatibility layer ระหว่าง migration
- server-side checks ต้องย้ายจาก `isHRD()` / `isIT()` ไปเป็น `hasPermission()` / `requirePermission()`
## Recommended Rollout
1. เพิ่ม permission catalog กลางใน `src/lib/auth`
2. เพิ่ม helper:
- `hasPermission()`
- `hasAnyPermission()`
- `requirePermission()`
3. เปลี่ยน route handlers ให้เช็ก permission ก่อน
4. เปลี่ยน nav/page visibility ให้ใช้ permission matrix
5. คง `businessRole` ไว้ชั่วคราวเพื่อไม่ให้ feature เดิมพัง
6. ค่อยทยอย refactor feature ให้ใช้ permission-first model เต็มรูปแบบ

View File

@@ -0,0 +1,487 @@
# Role Permission Migration Plan
วันที่จัดทำ: 2026-07-07
## Goal
ย้ายระบบสิทธิ์ปัจจุบันจากแนวทางที่พึ่งพา:
- `systemRole`
- `membership.role`
- `businessRole`
- helper แบบ `isHRD()` / `isIT()`
ไปสู่ระบบ `role + permission` ที่ตรวจสอบได้ชัดเจนและรองรับ workflow ใหม่ของโปรเจกต์
เอกสารนี้สรุปเป็นแบบ file-by-file ว่าควรแก้อะไรบ้างในโค้ดปัจจุบัน
## Migration Principles
1. API และ server ต้องใช้ permission เป็นตัวตัดสินสิทธิ์จริง
2. UI และ navigation เป็นเพียง layer สำหรับซ่อน/แสดง
3. `businessRole` ให้คงไว้ช่วง migration เพื่อ compatibility
4. ทุกการเปลี่ยน role model ต้องมี migration ของ `memberships.permissions`
## Phase 0: Schema And Contracts
### [src/db/schema.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/db/schema.ts)
Current:
- `membership_role` = `owner | admin | member`
- `system_role` = `standard | super_admin`
- `permissions` เป็น text array
Change:
- ขยาย `membership_role` หรือสร้าง enum ใหม่ให้รองรับ:
- `owner`
- `org_admin`
- `hrd_manager`
- `hrd_staff`
- `employee`
- คง `permissions` ไว้เป็น array แต่เปลี่ยน default generation ให้สอดคล้อง matrix ใหม่
- พิจารณาเพิ่ม migration สำหรับ backfill permissions ให้ membership เดิม
Deliverable:
- schema update
- drizzle migration
- backfill strategy สำหรับ membership เดิม
## Phase 1: Central Auth Model
### [src/lib/auth/roles.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/roles.ts)
Current:
- มี `BusinessRole = IT | HRD | EMPLOYEE`
- ใช้ `getBusinessRole()`, `isHRD()`, `isIT()`
- `getDefaultPermissionsForRole()` ยังรองรับเพียง admin/user แบบง่าย
Change:
- เพิ่ม type ใหม่:
- `SystemRole`
- `OrganizationRole`
- `Permission`
- แทนที่ default permission แบบเก่าด้วย permission catalog กลาง
- เพิ่ม helper:
- `getDefaultPermissionsForOrganizationRole(role)`
- `hasPermission(permissions, permission)`
- `hasAnyPermission(permissions, permissions[])`
- `hasAllPermissions(permissions, permissions[])`
- คง `getBusinessRole()` ไว้ช่วงเปลี่ยนผ่าน แต่ mark เป็น compatibility helper
- ลดการใช้ `isHRD()` / `isIT()` ลง โดย redirect ให้ไปใช้ permission helper
### New file: `src/lib/auth/permissions.ts`
Add:
- permission constants ทั้งระบบ
- grouped permission sets
- role-to-permission mapping
### [src/lib/auth/session.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/session.ts)
Current:
- มี `requireHRD()`, `requireIT()`, `requireUserManagementAccess()`
- เช็กสิทธิ์จาก role helper เป็นหลัก
Change:
- เพิ่ม:
- `requirePermission(permission)`
- `requireAnyPermission([...])`
- `requireAllPermissions([...])`
- ให้ `requireHRD()` / `requireIT()` กลายเป็น transitional wrapper หรือค่อยเลิกใช้
- ให้ `requireOrganizationAccess({ permission })` ใช้ permission matrix ใหม่
### [src/auth.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/auth.ts)
Current:
- session callback derive `businessRole`
- เติม `permissions` จาก membership หรือ default เดิม
Change:
- เปลี่ยน default permission sourcing ให้ใช้ role mapping ใหม่
- เพิ่ม `organizationRole` ลง session ให้ชัด
- คง `businessRole` ไว้ชั่วคราวเพื่อไม่ให้ UI เดิมพัง
- เตรียมรองรับ role ใหม่ใน session payload
### [src/types/next-auth.d.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/next-auth.d.ts)
Change:
- เพิ่ม `organizationRole`
- เปลี่ยน `permissions: string[]` เป็น type-safe alias ถ้าต้องการ
- คง `businessRole` ไว้ช่วงเปลี่ยนผ่าน
## Phase 2: Navigation And Access Metadata
### [src/config/nav-config.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts)
Current:
- ใช้ `businessRole`, `businessRoles`, `systemRole`
Change:
- เปลี่ยน metadata ให้รองรับ:
- `permission`
- `anyPermissions`
- `allPermissions`
- `organizationRole` เฉพาะบางกรณีถ้าจำเป็น
- ลดการใช้ `businessRole` ลง
- sync เมนูที่เปิดให้ employee ใช้จริง เช่น `Reports`, `Announcements`, `Notifications` กับ access rule ใหม่
### [src/types/index.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/types/index.ts)
Change:
- update nav access types ให้รองรับ permission-based metadata
### [src/hooks/use-nav.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/hooks/use-nav.ts)
Current:
- ใช้ `getBusinessRole()`, `hasBusinessRoleAccess()`, `systemRole`
Change:
- เพิ่มการเช็ก:
- `permission`
- `anyPermissions`
- `allPermissions`
- คง support ของ field เก่าไว้ช่วง migration ถ้าจำเป็น
- เปลี่ยน source of truth จาก `businessRole` เป็น `permissions`
## Phase 3: Page Guards
### [src/lib/auth/page-guards.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/page-guards.ts)
Change:
- เพิ่ม:
- `requirePermissionDashboardAccess(permission, fallback?)`
- `requireAnyPermissionDashboardAccess([...], fallback?)`
- ให้หน้าต่าง ๆ ที่เป็น HRD/IT เฉพาะทางย้ายไปใช้ permission guard
### Dashboard pages under `src/app/dashboard/**/page.tsx`
Current:
- หลายหน้าพึ่ง `requireEmployeeDashboardAccess()` หรือ `requireHRDDashboardAccess()`
Change:
- เปลี่ยนทีละหน้าให้ใช้ permission guard ที่ตรง feature มากขึ้น
Priority pages:
- `src/app/dashboard/users/page.tsx`
- `src/app/dashboard/employee-directory/page.tsx`
- `src/app/dashboard/courses/page.tsx`
- `src/app/dashboard/training-policy/page.tsx`
- `src/app/dashboard/pending-review/page.tsx`
- `src/app/dashboard/audit-logs/page.tsx`
- `src/app/dashboard/announcements/page.tsx`
- `src/app/dashboard/online-lessons/page.tsx`
- `src/app/dashboard/reports/page.tsx`
## Phase 4: Feature Server Data Helpers
### [src/features/announcements/server/announcement-data.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/announcements/server/announcement-data.ts)
Current:
- ใช้ `isHRD({ businessRole, systemRole })`
Change:
- ส่ง permission context เข้า helper แทน
- แยก:
- public reader
- all-status reader
- content manager
- ใช้ permission เช่น:
- `announcement:read_public`
- `announcement:read_all`
- `announcement:create`
- `announcement:publish`
### [src/features/online-lessons/server/online-lesson-data.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/online-lessons/server/online-lesson-data.ts)
Change:
- ทำแบบเดียวกับ announcements
- เลิกใช้ `isHRD()` เป็น primary gate
### [src/features/reports/server/report-data.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/reports/server/report-data.ts)
Current:
- ผสม role-based scope แบบ membership role
Change:
- แยก `reports:self_read` กับ `reports:organization_read`
- export ต้องใช้ `reports:export`
### [src/features/training-records/server/training-record-data.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/server/training-record-data.ts)
Change:
- เปลี่ยน review/manage scope ไปใช้ permission:
- `training_record:self_*`
- `training_record:read_all`
- `training_record:review`
- `training_record:manage_attachments`
### [src/features/training-matrix/server/training-matrix-data.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-matrix/server/training-matrix-data.ts)
Current:
- มี helper `canManageTrainingMatrix(role)`
Change:
- เปลี่ยนเป็น permission-based helper
### [src/features/users/server/user-data.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/users/server/user-data.ts)
Change:
- รองรับ role ใหม่ใน CRUD
- อัปเดตการ map/create/update membership
- อัปเดต default permissions ตอนสร้าง user
## Phase 5: API Route Handlers
### Announcements
Files:
- [src/app/api/announcements/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/announcements/route.ts)
- [src/app/api/announcements/[id]/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/announcements/[id]/route.ts)
- [src/app/api/announcements/[id]/attachment/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/announcements/[id]/attachment/route.ts)
Change:
- เปลี่ยนจาก `requireHRD()` หรือ `isHRD()` ไปใช้ permission checks
- แยก action ตาม permission เช่น create/edit/submit/publish/archive
- ระยะต่อไปเพิ่ม route action แยกสำหรับ workflow
### Online Lessons
Files:
- [src/app/api/online-lessons/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/online-lessons/route.ts)
- [src/app/api/online-lessons/[id]/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/online-lessons/[id]/route.ts)
Change:
- ใช้ permission checks แทน `isHRD()`
- แยก read_public / read_all / create / publish / archive
### Users
Files:
- [src/app/api/users/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/users/route.ts)
- [src/app/api/users/[id]/route.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/users/[id]/route.ts)
Change:
- เปลี่ยนจาก `requireUserManagementAccess()` ไปใช้ `users:*`, `roles:assign`, `permissions:assign`
### Employee Directory / Import / Master Review
Files:
- `src/app/api/employee-directory/**`
- `src/app/api/import-employees/**`
- `src/app/api/master-review/route.ts`
Change:
- ใช้:
- `employee_directory:read`
- `employee_directory:write`
- `employee_import:run`
- `employee_master_review:approve`
### Courses / Training Policy / Training Matrix
Files:
- `src/app/api/courses/**`
- `src/app/api/training-policies/**`
- `src/app/api/training-matrix/**`
Change:
- เปลี่ยนไปใช้ permission ของแต่ละ module โดยตรง
### Training Records
Files:
- `src/app/api/training-records/**`
Change:
- เปลี่ยน review / attachment / update / delete checks ไปใช้ permission model ใหม่
### Audit Logs
Files:
- `src/app/api/audit-logs/**`
Change:
- ใช้ `audit_logs:read`
### Reports
Files:
- `src/app/api/reports/**`
Change:
- ใช้:
- `reports:self_read`
- `reports:organization_read`
- `reports:export`
## Phase 6: Feature UI Components
### Announcements UI
Files:
- `src/features/announcements/components/**`
Change:
- เปลี่ยน visibility ของปุ่มและ action menu ให้ใช้ permission-based props
- หน้า employee กับ HRD ควรใช้ component เดิมได้ แต่แยก action ตาม permission
### Online Lessons UI
Files:
- `src/features/online-lessons/components/**`
Change:
- เปลี่ยน action menu และ create/manage flow ให้ใช้ permission-based props
### Training Records UI
Files:
- `src/features/training-records/components/**`
Change:
- เปลี่ยนปุ่ม review/edit/manage attachments ให้ใช้ permission context แทน business role
### Users UI
Files:
- `src/features/users/components/**`
Change:
- แบบฟอร์มและ action menu ต้องรองรับ role ใหม่
- ต้องมี UI สำหรับเลือก role และอาจรวม permission assignment ถ้าจะเปิดใช้งานรายคน
## Phase 7: Notifications And Audit
### [src/features/notifications/server/notification-service.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/server/notification-service.ts)
Change:
- เพิ่ม notification types ที่สะท้อน workflow ใหม่
- เพิ่ม helper สำหรับ notify ตาม role/permission group ถ้าจำเป็น
### [src/features/audit-logs/server/audit-service.ts](/D:/WY-2569/HRD/training-system-minimal-refactor/src/features/audit-logs/server/audit-service.ts)
Change:
- เพิ่ม audit action ใหม่สำหรับ workflow/action ที่อ้างกับ permission model
- พิจารณาเพิ่ม `actor_role` หรือ `actor_permissions` ใน payload audit ถ้าต้องการ trace ละเอียดขึ้น
## Phase 8: Documentation And Compatibility Cleanup
### Docs to update
Files:
- [docs/nav-rbac.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md)
- [docs/PROJECT_ARCHITECTURE.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/PROJECT_ARCHITECTURE.md)
- [docs/AI_DEVELOPMENT_GUIDE.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/AI_DEVELOPMENT_GUIDE.md)
- [docs/role-permission-review.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/role-permission-review.md)
Change:
- update ให้ตรงกับ permission-first model
### Compatibility cleanup
Files to revisit later:
- `src/lib/auth/roles.ts`
- `src/hooks/use-nav.ts`
- feature components ที่ยังส่ง `businessRole` / `systemRole` ลงไปหลายชั้น
Final cleanup:
- ลดการใช้ `businessRole` ให้เหลือเฉพาะ temporary adapter
- ลบ logic `isHRD()` / `isIT()` ที่ไม่จำเป็น
## Suggested Execution Order
1. `schema.ts` + migration
2. `permissions.ts` + `roles.ts`
3. `auth.ts` + `session.ts` + `next-auth.d.ts`
4. `types/index.ts` + `nav-config.ts` + `use-nav.ts`
5. API route handlers
6. feature server helpers
7. feature UI
8. docs cleanup
## High-Risk Areas
1. `src/auth.ts`
เพราะกระทบทุก session
2. `src/lib/auth/session.ts`
เพราะเป็นจุดศูนย์กลางของ server-side authorization
3. `src/config/nav-config.ts` และ `src/hooks/use-nav.ts`
เพราะมีความไม่ตรงกันระหว่างเมนูและสิทธิ์จริงอยู่แล้ว
4. `src/features/users/server/user-data.ts`
เพราะเกี่ยวกับ role assignment และ membership write path
5. `src/app/api/announcements/**` และ `src/app/api/online-lessons/**`
เพราะจะเป็น feature แรกที่ใช้ workflow permission ใหม่แบบจริงจัง
## Done Criteria
ถือว่าย้ายสำเร็จเมื่อ:
- API สำคัญทั้งหมดตรวจ permission โดยตรง
- nav และ page guard ใช้ permission model เดียวกัน
- role ใหม่ถูกสร้าง/แก้ไขได้จาก user management
- default permissions ของแต่ละ role ถูก seed/backfill ครบ
- employee ยังเห็นเฉพาะข้อมูลที่ควรเห็น
- `businessRole` ไม่ใช่ source of truth หลักอีกต่อไป

View File

@@ -0,0 +1,257 @@
# Role Permission Review
วันที่ตรวจสอบ: 2026-06-30
## Scope
เอกสารนี้เป็นการตรวจสอบระบบสิทธิ์การเข้าถึงของ 3 กลุ่มหลัก:
- Employee
- HRD Admin
- IT Admin
ขอบเขตที่ตรวจ:
- การมองเห็นเมนูใน sidebar
- การกัน direct URL ที่ระดับ page
- การกันสิทธิ์ที่ระดับ API / server-side
- การจำกัดขอบเขตข้อมูล
- ความสอดคล้องระหว่าง requirement กับ implementation ปัจจุบัน
ไฟล์หลักที่ใช้ตรวจ:
- `src/config/nav-config.ts`
- `src/hooks/use-nav.ts`
- `src/lib/auth/roles.ts`
- `src/lib/auth/session.ts`
- `src/lib/auth/page-guards.ts`
- `src/proxy.ts`
- `src/app/dashboard/**`
- `src/app/api/**`
## Requirement Summary
จากแผนงานที่ให้ตรวจ ระบบนี้ควรแยกสิทธิ์อย่างน้อยเป็น 3 ระดับ:
- Employee เห็นเฉพาะส่วนที่ใช้กับตัวเอง และข้อมูลต้องถูก scope เป็นของตัวเอง
- HRD Admin จัดการข้อมูลฝั่งงานอบรมและข้อมูลบุคลากรในองค์กรได้
- IT Admin จัดการสิทธิ์ระดับสูงกว่า HRD และเข้าถึงส่วน technical / audit ได้
ประเด็นสำคัญที่แผนเน้นเป็นพิเศษ:
- เมนูและหน้าจอต้องสอดคล้องกับสิทธิ์จริง
- ห้ามพึ่งแค่ซ่อนเมนู ต้องกันที่ server ด้วย
- ข้อมูลของ Employee ต้องไม่ทะลุไปเห็นของคนอื่น
- ฟังก์ชัน `Import พนักงาน` ควรถูกทบทวน/ถอดออกจาก flow ถ้าไม่ได้ต้องการใช้งานแล้ว
## Findings
### 1. High: หน้าและ API `Import พนักงาน` ยังเปิดใช้งานอยู่
สถานะ: `Fail`
หลักฐาน:
- [src/app/dashboard/employee-directory/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/employee-directory/page.tsx:54) ยังมีปุ่มลิงก์ไป `/dashboard/import-employees`
- [src/app/dashboard/import-employees/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/import-employees/page.tsx:5) ยังเปิดหน้า import
- [src/app/api/import-employees/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/import-employees/route.ts:401) ยังเปิด API import โดยใช้ `requireHRD()`
ผลกระทบ:
- ถ้า requirement ล่าสุดต้องการยกเลิก flow นี้ ปัจจุบันถือว่ายังไม่ปิดจริง
- ผู้ใช้ HRD / IT ยังเข้าใช้งานได้ผ่านปุ่ม, direct URL และ API
คำแนะนำ:
- ถ้าต้องการถอดฟังก์ชันนี้จริง ควรลบปุ่ม, ปิดหน้า route, ปิด API และตรวจ flow ที่เกี่ยวข้องกับ `master-review` ต่อด้วย
### 2. Medium: สิทธิ์ฝั่ง Employee สำหรับเมนูบางหน้าไม่สอดคล้องกับสิทธิ์จริงของหน้า
สถานะ: `Need Review`
หลักฐาน:
- เมนู `Reports` ใน [src/config/nav-config.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts:103) จำกัดเป็น `businessRole: 'HRD'`
- แต่หน้า reports ใช้ [src/app/dashboard/reports/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/reports/page.tsx:20) ผ่าน `requireEmployeeDashboardAccess()`
- data scope ของ reports ก็รองรับ employee แบบ self-scope ใน [src/features/reports/server/report-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/reports/server/report-data.ts:126)
ข้อสังเกตเพิ่ม:
- หน้า `Announcements` ใช้ `requireEmployeeDashboardAccess()` ใน [src/app/dashboard/announcements/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/page.tsx:18)
- หน้า `Notifications` ใช้ `requireEmployeeDashboardAccess()` ใน [src/app/dashboard/notifications/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/notifications/page.tsx:16)
- แต่ใน `nav-config.ts` เมนู `Announcements` ถูกวางไว้ในกลุ่ม HRD/IT เท่านั้น และ `Notifications` ไม่มีใน sidebar
ผลกระทบ:
- สิทธิ์จริงของระบบกับสิ่งที่ผู้ใช้เห็นในเมนูไม่ตรงกัน
- ผู้ใช้ employee อาจ “เข้าได้แต่ไม่เห็นเมนู”
คำแนะนำ:
- ตกลงให้ชัดว่าหน้าใดควรเป็น employee-facing
- จากนั้น sync ระหว่าง `nav-config.ts` กับ page/API permission ให้ตรงกัน
### 3. Medium: โมเดลสิทธิ์ IT กับ HRD ยังพึ่งการตีความทางอ้อมมากกว่าตารางสิทธิ์ที่ชัด
สถานะ: `Need Review`
หลักฐาน:
- `super_admin` ถูก map เป็น `IT` ใน [src/lib/auth/roles.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/roles.ts:48)
- `isHRD()` คืนค่า `true` ให้ทั้ง HRD และ super admin ใน [src/lib/auth/roles.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/roles.ts:63)
- `requireHRD()` จึงเปิดให้ IT ผ่านได้ด้วยใน [src/lib/auth/session.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/session.ts:143)
ผลกระทบ:
- ตอนนี้ระบบทำงานแบบ “IT เข้าส่วน HRD ได้ทั้งหมด” ซึ่งอาจถูกต้องก็ได้
- แต่ถ้าอนาคตอยากแยกสิทธิ์ HRD กับ IT ให้ต่างกันมากขึ้น จะตามแก้ยาก เพราะ logic ผูกกันอยู่หลายชั้น
คำแนะนำ:
- ถ้าต้องการให้ IT = สิทธิ์มากกว่า HRD แบบตั้งใจ ถือว่าใช้งานได้
- แต่ถ้าต้องการ matrix สิทธิ์ที่ตรวจสอบง่าย ควรมี role-permission matrix กลางที่อ่านแล้วตอบได้ทันทีว่าแต่ละ role ทำอะไรได้บ้าง
### 4. Low: เอกสาร RBAC ปัจจุบันไม่ตรงกับ implementation บางจุด
สถานะ: `Need Review`
หลักฐาน:
- [docs/nav-rbac.md](/d:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md:85) ระบุว่า HRD เข้าถึง `Audit Logs` ได้
- แต่เมนู `Audit Logs` ใน [src/config/nav-config.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts:94) จำกัด `systemRole: 'super_admin'`
- และหน้า audit log ใช้ [src/app/dashboard/audit-logs/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/audit-logs/page.tsx:16) ผ่าน `requireITDashboardAccess()`
ผลกระทบ:
- คนดูเอกสารอาจเข้าใจสิทธิ์ผิด
- เสี่ยงตัดสินใจต่อยอด feature จากเอกสารที่ไม่ตรงโค้ดจริง
คำแนะนำ:
- อัปเดตเอกสารให้ตรงกับ implementation ปัจจุบัน
## Menu Visibility Review
### Employee
ผลที่พบ:
- เห็น `Overview`, `Training Records`, `Online Lessons`
- ไม่เห็น `Reports` จาก sidebar แม้ว่าหน้า reports จะเข้าได้จริง
- ไม่เห็น `Announcements` และ `Notifications` จาก sidebar แม้ว่าหน้าเหล่านี้เปิดสิทธิ์ employee
สรุป: `Partial`
### HRD Admin
ผลที่พบ:
- เห็น `Pending Review`
- เห็นกลุ่ม `ข้อมูลหลัก` และเข้าถึง `Employee Directory`, `Courses`, `Training Policy`, `Announcements`
- เข้า `Import Employees` ได้ผ่าน direct URL และจากปุ่มในหน้า employee directory
- ไม่เห็น `Users` เพราะเมนูนั้นเปิดเฉพาะ IT
สรุป: `Mostly Pass`
### IT Admin
ผลที่พบ:
- เห็นเมนู `Users`
- เห็นส่วนของ HRD ได้ทั้งหมดผ่าน role mapping
- เห็น `Audit Logs` เฉพาะ `super_admin`
สรุป: `Pass`
## Route Protection Review
### ผ่าน
- หน้า employee-facing ใช้ `requireEmployeeDashboardAccess()`
- หน้า HRD ใช้ `requireHRDDashboardAccess()`
- หน้า IT ใช้ `requireITDashboardAccess()` หรือ `requireUserManagementDashboardAccess()`
หลักฐานสำคัญ:
- [src/lib/auth/page-guards.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/page-guards.ts:23)
- [src/app/dashboard/users/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/users/page.tsx:18)
- [src/app/dashboard/employee-directory/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/employee-directory/page.tsx:20)
- [src/app/dashboard/pending-review/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/pending-review/page.tsx:21)
- [src/app/dashboard/audit-logs/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/audit-logs/page.tsx:16)
### ข้อสังเกต
- [src/proxy.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/proxy.ts:27) กันได้แค่ “ล็อกอินหรือยัง”
- การกัน “สิทธิ์ role ไหนเข้าได้” ยังต้องพึ่ง page guard และ API handler ต่อ ซึ่งตอนนี้ถือว่าทำถูกทิศทางแล้ว
สรุป: `Pass`
## API / Server Action Permission Review
### ผ่าน
- `Users API` เปิดเฉพาะ IT ผ่าน `requireUserManagementAccess()`
- [src/app/api/users/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/users/route.ts:23)
- [src/app/api/users/[id]/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/users/[id]/route.ts:25)
- `Audit Logs API` เปิดเฉพาะ IT ผ่าน `requireIT()`
- [src/app/api/audit-logs/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/audit-logs/route.ts:8)
- `Employee Directory`, `Courses`, `Training Policies`, `Import Employees`, `Master Review` เปิดเฉพาะ HRD/IT ผ่าน `requireHRD()`
### ผ่านแบบมีเงื่อนไข
- `Training Records` ใช้ `requireOrganizationAccess()` แล้วคุมต่อด้วย scope และ review permission ภายใน service
- `Reports Export` กัน employee ไม่ให้ export รายงานรวม โดยอนุญาตเฉพาะ `employeeTranscript`
- [src/app/api/reports/export/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/reports/export/route.ts:42)
สรุป: `Pass`
## Data Scope Review
### Employee
ผ่าน:
- training records ถูก scope ตาม employee mapping ของ user
- [src/features/training-records/server/training-record-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/server/training-record-data.ts:224)
- reports ถูก scope ตาม employee ของตัวเอง ถ้า role ไม่ใช่ owner/admin
- [src/features/reports/server/report-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/reports/server/report-data.ts:126)
### HRD / IT
ผ่าน:
- reports ระดับองค์กรเปิดให้ membership role `owner/admin`
- training review เปิดเฉพาะ role ที่ review ได้
- [src/app/api/training-records/[id]/review/route.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/api/training-records/[id]/review/route.ts:31)
สรุป: `Pass`
## Hardcoded Roles Review
สิ่งที่พบ:
- role string หลักกระจุกอยู่ใน `src/lib/auth/roles.ts`
- แต่ยังมีการอ้าง role ตรง ๆ ซ้ำใน `nav-config.ts`, `session.ts`, เอกสาร, และบาง feature
ประเมิน:
- ยังไม่ถึงขั้นกระจายมั่ว
- แต่ยังไม่มี permission matrix กลางที่เป็น single source of truth
สรุป: `Acceptable but should improve later`
## Summary
ภาพรวมปัจจุบัน:
- ระบบกันสิทธิ์ที่ระดับ page และ API ถือว่าใช้ได้
- data scope ของ employee ฝั่ง training records และ reports ถือว่าปลอดภัยในระดับหนึ่ง
- จุดที่ไม่ตรง requirement ชัดที่สุดคือ `Import พนักงาน` ยังไม่ถูกถอดออกจริง
- อีกจุดที่ควรรีบจัดระเบียบคือความไม่ตรงกันระหว่าง “เมนูที่เห็น” กับ “สิทธิ์จริงของหน้า” โดยเฉพาะ `Reports`, `Announcements`, `Notifications`
## Recommended Next Actions
1. ตัดสินใจให้ชัดว่า `Import พนักงาน` จะเก็บหรือจะถอด แล้วปิดทั้ง UI, route, API ให้ครบ
2. ทำ matrix สิทธิ์ของ `Employee / HRD / IT` แบบสั้น ๆ แล้วใช้เป็นตัวเทียบกับ `nav-config.ts`
3. sync `nav-config.ts` กับ page/API permission ให้ผู้ใช้เห็นเมนูตรงกับสิทธิ์จริง
4. อัปเดต `docs/nav-rbac.md` ให้ตรงกับโค้ดปัจจุบัน

View File

@@ -0,0 +1,147 @@
# Sprint 4 Review: Training Policy and Course Master
## 1. Summary of changes
- Implemented the HRD-only Training Policy module at `/dashboard/training-policy`.
- Added yearly training policy create/update flow with Zod validation.
- Enforced `kHours + sHours + aHours === totalHours`.
- Added active policy handling so the active record drives dashboard targets.
- Updated dashboard summary cards to use Training Policy values.
- Enhanced Course Master to support:
- `courseCode`
- `courseName` via existing `name`
- `description`
- `defaultCategory` via existing `category`
- `defaultHours` via existing `standardHours`
- `provider` via existing `organizer`
- `status` via existing `isActive`
- `mandatory` via existing `isRequired`
- `certificateRequired`
- `validityMonths` via existing `certificateValidityMonths`
- `trainingCost`
- Added loading, empty, success, and error states for Sprint 4 pages.
## 2. Files changed
### Training Policy
- `src/app/api/training-policies/route.ts`
- `src/app/api/training-policies/[id]/route.ts`
- `src/app/dashboard/training-policy/page.tsx`
- `src/app/dashboard/training-policy/loading.tsx`
- `src/app/dashboard/training-policy/error.tsx`
- `src/features/training-policy/api/types.ts`
- `src/features/training-policy/api/service.ts`
- `src/features/training-policy/api/queries.ts`
- `src/features/training-policy/api/mutations.ts`
- `src/features/training-policy/components/training-policy-page.tsx`
- `src/features/training-policy/components/training-policy-form.tsx`
- `src/features/training-policy/constants/training-policy-defaults.ts`
- `src/features/training-policy/schemas/training-policy.ts`
- `src/features/training-policy/server/training-policy-data.ts`
### Course Master
- `src/app/api/courses/route.ts`
- `src/app/api/courses/[id]/route.ts`
- `src/app/dashboard/courses/loading.tsx`
- `src/app/dashboard/courses/error.tsx`
- `src/app/dashboard/courses/[courseId]/loading.tsx`
- `src/app/dashboard/courses/[courseId]/error.tsx`
- `src/features/courses/api/types.ts`
- `src/features/courses/components/course-form.tsx`
- `src/features/courses/components/course-tables/columns.tsx`
- `src/features/courses/components/course-tables/index.tsx`
- `src/features/courses/components/course-tables/options.tsx`
- `src/features/courses/constants/course-options.ts`
- `src/features/courses/schemas/course.ts`
- `src/features/courses/server/course-data.ts`
### Dashboard usage
- `src/features/overview/server/overview-data.ts`
## 3. Database changes
- No new schema changes were required.
- Sprint 4 reused the existing `training_policies` table.
- Sprint 4 reused existing `courses` columns:
- `course_code`
- `category`
- `standard_hours`
- `organizer`
- `is_required`
- `certificate_required`
- `certificate_validity_months`
- `training_cost`
## 4. Migration commands
- No new migration is required for this sprint.
- If your database has not yet applied the current schema in the repository, run:
```bash
npm run migrate
```
## 5. New routes
- `GET /api/training-policies`
- `POST /api/training-policies`
- `PATCH /api/training-policies/[id]`
- `/dashboard/training-policy`
## 6. Validation rules
### Training Policy
- `year` is required
- `totalHours` is required and must be greater than `0`
- `kHours` is required and must be `>= 0`
- `sHours` is required and must be `>= 0`
- `aHours` is required and must be `>= 0`
- `kHours + sHours + aHours` must equal `totalHours`
- `year` must be unique per organization
### Course Master
- `courseName` maps to `name` and must be at least 2 characters
- `defaultCategory` maps to `category` and is required
- `defaultHours` maps to `standardHours` and must be `>= 0`
- `validityMonths` maps to `certificateValidityMonths` and must be an integer `>= 0`
- `trainingCost` must be `>= 0` when provided
- `courseCode` is optional and limited to 50 characters
## 7. Permission rules
- Training Policy page and APIs require HRD access through existing Auth.js session helpers.
- Employee users cannot access `/dashboard/training-policy`.
- Course Master remains HRD-only through the existing route and API guards.
- Dashboard policy usage respects the user's existing organization/session scope.
## 8. Manual test checklist
- [ ] HRD can open Training Policy page.
- [ ] Employee cannot open Training Policy page.
- [ ] HRD can create policy.
- [ ] HRD can edit policy.
- [ ] System blocks invalid K/S/A sum.
- [ ] Dashboard uses policy values.
- [ ] HRD can create course.
- [ ] HRD can edit course.
- [ ] Course Master supports `certificateRequired`.
- [ ] Course Master supports `trainingCost`.
## 9. Known limitations
- Course category input is now aligned to `K/S/A`, but legacy category values are still preserved for existing rows.
- Training Policy currently supports create and update only; delete/archive flow is not part of Sprint 4.
- Dashboard summary cards use the active policy, or the shared default example if no policy row exists yet.
## 10. Next recommended sprint
- Add audit logging for Training Policy changes.
- Add a dedicated policy history detail or archive flow.
- Expand dashboard policy reporting to show K/S/A completion progress, not only total-hour target progress.
- Add course import/export support if HRD master-data volume is expected to grow.

View File

@@ -0,0 +1,258 @@
# Sprint 5 Review: Employee Import, Master Review, and Thai UI Localization
## 1. Summary
- Added the HRD-only Employee Import page at `/dashboard/import-employees`.
- Added Excel template download and `.xlsx` import flow.
- Added row validation, duplicate detection, and Thai import result summary.
- Imported employee runtime data into existing `users` and `memberships` tables.
- Added the HRD-only Master Review page at `/dashboard/master-review`.
- Added pending review handling for:
- companies
- departments
- positions
- Added approve/reject actions for pending master data.
- Converted visible TMS UI text in the implemented modules and shared dashboard surfaces to Thai.
- Centralized repeated Thai labels in `src/constants/thai-labels.ts`.
## 2. Files changed
### Employee Import
- `src/app/api/import-employees/route.ts`
- `src/app/api/import-employees/template/route.ts`
- `src/app/dashboard/import-employees/page.tsx`
- `src/app/dashboard/import-employees/loading.tsx`
- `src/app/dashboard/import-employees/error.tsx`
- `src/features/import-employees/api/types.ts`
- `src/features/import-employees/api/service.ts`
- `src/features/import-employees/api/queries.ts`
- `src/features/import-employees/api/mutations.ts`
- `src/features/import-employees/components/employee-import-page.tsx`
### Master Review
- `src/app/api/master-review/route.ts`
- `src/app/dashboard/master-review/page.tsx`
- `src/app/dashboard/master-review/loading.tsx`
- `src/app/dashboard/master-review/error.tsx`
- `src/features/import-employees/components/master-review-page.tsx`
### Thai localization and shared UI
- `src/constants/thai-labels.ts`
- `src/lib/api-client.ts`
- `src/config/nav-config.ts`
- `src/components/layout/app-sidebar.tsx`
- `src/components/layout/user-nav.tsx`
- `src/components/org-switcher.tsx`
- `src/components/search-input.tsx`
- `src/components/modal/alert-modal.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/components/ui/table/data-table-toolbar.tsx`
### Training records and pending review localization
- `src/app/dashboard/training-records/loading.tsx`
- `src/app/dashboard/training-records/[trainingRecordId]/review/page.tsx`
- `src/app/dashboard/training-records/[trainingRecordId]/review/loading.tsx`
- `src/app/dashboard/training-records/[trainingRecordId]/review/error.tsx`
- `src/app/dashboard/pending-review/page.tsx`
- `src/app/dashboard/pending-review/loading.tsx`
- `src/app/dashboard/pending-review/error.tsx`
- `src/features/training-records/constants/training-record-options.ts`
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/components/pending-review-columns.tsx`
- `src/features/training-records/components/pending-review-table.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-review-form.tsx`
- `src/features/training-records/components/training-record-review-page.tsx`
- `src/features/training-records/components/training-record-certificate-manager.tsx`
- `src/features/training-records/components/training-record-tables/columns.tsx`
- `src/features/training-records/components/training-record-tables/index.tsx`
- `src/features/training-records/components/training-record-view-page.tsx`
### Courses, reports, overview, and training policy localization
- `src/app/dashboard/courses/page.tsx`
- `src/app/dashboard/courses/loading.tsx`
- `src/app/dashboard/courses/error.tsx`
- `src/app/dashboard/reports/page.tsx`
- `src/app/dashboard/reports/loading.tsx`
- `src/app/dashboard/reports/error.tsx`
- `src/app/dashboard/training-policy/page.tsx`
- `src/app/dashboard/training-policy/loading.tsx`
- `src/app/dashboard/training-policy/error.tsx`
- `src/app/dashboard/overview/layout.tsx`
- `src/app/dashboard/overview/error.tsx`
- `src/features/courses/components/course-form.tsx`
- `src/features/courses/components/course-tables/columns.tsx`
- `src/features/overview/server/overview-data.ts`
- `src/features/overview/components/bar-graph.tsx`
- `src/features/overview/components/area-graph.tsx`
- `src/features/overview/components/pie-graph.tsx`
- `src/features/overview/components/recent-sales.tsx`
- `src/features/reports/components/report-table-card.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/server/report-data.ts`
- `src/features/training-policy/components/training-policy-form.tsx`
- `src/features/training-policy/components/training-policy-page.tsx`
### Dependency change
- `package.json`
- lockfile updated by `npm install xlsx`
## 3. Database changes
- No schema migration was added in Sprint 5.
- No table names were changed.
- No column names were changed.
- Employee import reuses existing runtime tables:
- `users`
- `memberships`
- Master review reuses existing flags and master tables:
- `organizations.pending_master_review`
- `departments.pending_master_review`
- `positions.pending_master_review`
## 4. Routes added
- `POST /api/import-employees`
- `GET /api/import-employees/template`
- `GET /api/master-review`
- `PATCH /api/master-review`
- `/dashboard/import-employees`
- `/dashboard/master-review`
## 5. Thai localization changes
- Sidebar labels changed to Thai.
- Page titles and descriptions changed to Thai.
- Form labels and placeholders changed to Thai.
- Table headers and filter labels changed to Thai.
- Buttons and action labels changed to Thai.
- Status badges changed to Thai.
- Toast messages changed to Thai.
- Validation messages changed to Thai.
- Empty, loading, and error states changed to Thai.
- Dashboard overview cards and chart labels changed to Thai.
- CSV export headers changed to Thai.
- Import template headers changed to Thai.
## 6. Label mapping added
- Added central mapping file: `src/constants/thai-labels.ts`
- Reused helper functions:
- `getTrainingStatusLabel()`
- `getTrainingTypeLabel()`
- `getTrainingCategoryLabel()`
- `getRoleLabel()`
- `getActiveStatusLabel()`
- Added Thai import template header constants and import status normalization.
## 7. Import template format
Required Thai headers:
1. `รหัสพนักงาน`
2. `ชื่อพนักงาน`
3. `อีเมล`
4. `บริษัท`
5. `แผนก`
6. `ตำแหน่ง`
7. `สถานะ`
Supported file type:
- `.xlsx`
Supported status values:
- `ใช้งาน`
- `ไม่ใช้งาน`
- `active`
- `inactive`
## 8. Validation rules
### Employee import
- File is required.
- File must be `.xlsx`.
- Worksheet must exist.
- File must contain rows.
- Headers must match the Thai template headers.
- Required fields per row:
- employee code
- employee name
- company
- department
- position
- status
- Email is optional, but when present it must be valid.
- Duplicate employee codes in the same file are rejected.
- Duplicate emails in the same file are rejected.
- Status must normalize to active or inactive.
### Training record review localization
- Approved hours must be at least `0.5`.
- Reject reason is required when rejecting.
- Thai validation messages are returned through the existing form flow.
## 9. Permission rules
- Employee Import page and APIs require HRD access through existing Auth.js session helpers.
- Master Review page and APIs require HRD access through existing Auth.js session helpers.
- Employee users cannot access `/dashboard/import-employees`.
- Employee users cannot access `/dashboard/master-review`.
- Imported records stay scoped to the active organization.
- Master review approve/reject updates are scoped to the active organization.
## 10. Manual test checklist
- [ ] HRD can open Employee Import page.
- [ ] Employee cannot open Employee Import page.
- [ ] HRD can download Thai Excel template.
- [ ] HRD can upload employee Excel.
- [ ] System validates required fields.
- [ ] System shows import result in Thai.
- [ ] New company/department/position becomes pending master review.
- [ ] HRD can open Master Review page.
- [ ] HRD can approve pending master data.
- [ ] HRD can reject pending master data.
- [ ] Sidebar menu is displayed in Thai.
- [ ] Training status badges are displayed in Thai.
- [ ] Training type labels are displayed in Thai.
- [ ] Form validation messages are displayed in Thai.
- [ ] Toast messages are displayed in Thai.
- [ ] Dashboard labels are displayed in Thai.
- [ ] Existing routes still work.
## 11. Known limitations
- There is no dedicated company master table in the current schema.
- Company review is therefore handled by comparing imported `users.company_name` values against the active organization name.
- Approving a pending company updates `organizations.name` for the active organization.
- Rejecting a pending company normalizes affected imported users back to the current organization name.
- Imported users are created with generated password hashes so the runtime user row is valid, but credential onboarding or password reset flow is still a separate operational step.
- Employee import currently supports `.xlsx` only; `.xls` and `.csv` are not included in this sprint.
## 12. Next recommended sprint
- Add audit log entries for employee import and master review actions.
- Add import history detail page from `import_jobs`.
- Add downloadable error report for failed import rows.
- Add a dedicated company master entity if company approval becomes a first-class workflow.
- Complete Thai localization for remaining non-TMS template/demo pages if those pages will stay in active use.
## 13. Migration command
- No new database migration is required for Sprint 5.
- If your environment has not installed the new Excel dependency yet, run:
```bash
npm install
```

View File

@@ -0,0 +1,159 @@
# Sprint 6.11 Identity / Employee Cleanup Review
## Summary
This pass moves the system closer to the target architecture where:
- `users` is login identity
- `employees` is employee master
- training ownership is based on `employeeId`
- user actions remain tracked separately
The work completed in this pass focuses on the highest-risk operational paths first:
- schema support for user-to-employee mapping
- stopping employee import from auto-creating users
- employee-owned training records
- employee-based employee directory
- employee-based training matrix compliance
## Migration Changes
Migration file:
- `drizzle/0010_romantic_marauders.sql`
Changes included:
- create `user_employee_maps`
- make `training_records.user_id` nullable
- make `employee_training_targets.user_id` nullable
- add `training_records.submitted_by_user_id`
- add `training_records.reviewed_by_user_id`
- add unique index `employee_training_targets_org_employee_year_idx`
- backfill `user_employee_maps` from existing `users.employee_id`
- backfill `submitted_by_user_id` from `created_by`
- backfill `reviewed_by_user_id` from `reviewed_by`
## Code Changes
### Identity / Mapping
- `src/db/schema.ts`
- `src/features/employees/server/employee-identity-linking.ts`
Added:
- `user_employee_maps`
- helper to resolve employee ids for a user
- helper to maintain mapping during import or login linking
### Employee Import
- `src/app/api/import-employees/route.ts`
Changed behavior:
- import upserts into `employees`
- import upserts `employee_training_targets` by `employeeId`
- import links existing users when employee code matches
- import no longer auto-creates login users
### Employees API
- `src/app/api/employees/route.ts`
- `src/features/employees/server/employee-data.ts`
Changed behavior:
- employees API is active again
- `mode=options` returns active employee options for HRD workflows
### Training Records
- `src/features/training-records/api/types.ts`
- `src/features/training-records/api/service.ts`
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-view-page.tsx`
- `src/features/training-records/server/training-record-data.ts`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/dashboard/training-records/[trainingRecordId]/page.tsx`
Changed behavior:
- training record create/update now uses `employeeId` as owner
- `submittedByUserId` is written on create
- `reviewedByUserId` is written on review
- employee self-scope uses mapped employee ids
- HRD employee selector now reads from `employees`
### Employee Directory
- `src/features/employee-directory/server/employee-directory-data.ts`
- `src/features/employee-directory/components/employee-directory-detail-page.tsx`
Changed behavior:
- directory detail reads from `employees`
- training summary and transcript use `training_records.employee_id`
### Training Targets
- `src/features/training-policy/server/employee-training-target-data.ts`
Changed behavior:
- added employee-based target resolution
- user-based target resolution now falls through employee mapping
### Training Matrix
- `src/features/training-matrix/server/training-matrix-data.ts`
Changed behavior:
- compliance calculation now uses `employees`
- approved-course ownership now uses `training_records.employee_id`
## Regression Checklist
1. Run `npm run migrate`
2. Sign in with an existing credentials user
3. Import employee Excel with no pre-existing user account
4. Verify employee rows are created/updated in `employees`
5. Verify no new `users` row is auto-created from import
6. Verify `employee_training_targets` rows are created using `employee_id`
7. Verify existing users with matching employee code are linked in `user_employee_maps`
8. As employee user with a linked employee profile, create a training record
9. Verify `training_records.employee_id` is set
10. Verify `training_records.submitted_by_user_id` is set
11. Review a pending record as HRD
12. Verify `training_records.reviewed_by_user_id` is set
13. Open employee directory detail and verify totals still load
14. Open training matrix compliance and verify rows still load
## Verification
Verified in this pass:
- `npx tsc --noEmit`
## Known Remaining Work
This pass does **not** fully migrate every aggregation path yet.
Still recommended for the next pass:
- move `overview` fully to `employees` + `training_records.employee_id`
- move `reports` fully to `employees` + `training_records.employee_id`
- reduce remaining dependency on `users.employeeCode` as compatibility fallback
- add Keycloak runtime mapping using `user_employee_maps`
## Risk Notes
- some runtime paths still keep compatibility writes to legacy columns
- `users.employeeId` still exists as fallback and compatibility support
- notifications still depend on login users, so employees without user accounts will not receive login-bound notifications

View File

@@ -0,0 +1,54 @@
# Sprint 6.12.1 Responsive DataTable Review
## Pages checked
- Training Records
- Pending Review
- Employee Directory
- Users
- Reports
- Shared reusable DataTable consumers
- Audit Logs
- Courses
- Employees
- Training Matrix
## Files changed
- `src/components/layout/page-container.tsx`
- `src/components/ui/table.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/components/ui/table/data-table-toolbar.tsx`
- `src/features/employee-directory/components/employee-directory-toolbar.tsx`
- `src/features/training-records/components/training-record-tables/columns.tsx`
- `src/features/training-records/components/training-record-tables/training-record-image-preview.tsx`
- `src/features/reports/components/report-table-card.tsx`
## Responsive fixes
- Kept the page shell anchored with `flex-1 min-w-0` so child table layouts can shrink inside the content area.
- Moved shared DataTable scrolling to a native `w-full overflow-x-auto` wrapper so only the table region scrolls horizontally.
- Standardized table sizing around `Table className="min-w-full"` in shared DataTable and report tables.
- Updated shared toolbar text filters to use `w-full max-w-sm` instead of fixed responsive widths.
- Updated Employee Directory search input to use `w-full max-w-sm`.
- Updated pagination layout to wrap and stay inside its container on narrow screens.
## Overflow fixes
- Added `overflow-hidden` protection to report table cards.
- Prevented page-level overflow by keeping DataTable containers `min-w-0 max-w-full overflow-hidden`.
- Reduced forced table expansion by removing global `whitespace-nowrap` from table cells.
- Fixed Training Records certificate preview column to a stable `w-20 h-20` footprint so it does not push the table wider than necessary.
## Manual regression checklist
- Verify the dashboard page itself does not scroll horizontally on mobile widths.
- Verify only the table region scrolls horizontally when columns exceed available width.
- Verify Training Records pagination remains fully visible at small widths.
- Verify Pending Review filters and pagination stay inside the card/container.
- Verify Employee Directory search and filters wrap cleanly without clipping.
- Verify Users table and pagination stay inside the content area.
- Verify Reports table cards scroll only inside the table wrapper.
- Verify pinned action columns still render correctly after shared DataTable wrapper changes.
- Verify certificate previews still open the dialog and render thumbnail images correctly.

View File

@@ -0,0 +1,170 @@
# Sprint 6.12 Employee Directory UI Review
## 1. Summary
Employee Directory was redesigned into a dedicated HR Employee Master screen backed by `employees` as the primary source.
This update separates Employee Directory from user management and adds:
- HR-oriented page header actions
- richer employee table with training-hour summary columns
- search and filter controls for employee master data
- employee create/edit flows under Employee Directory routes
- employee-specific K/S/A target management
- improved employee detail presentation
## 2. Files Changed
Employee Directory pages and UI:
- `src/app/dashboard/employee-directory/page.tsx`
- `src/app/dashboard/employee-directory/[id]/page.tsx`
- `src/app/dashboard/employee-directory/create/page.tsx`
- `src/app/dashboard/employee-directory/[id]/edit/page.tsx`
- `src/app/dashboard/employee-directory/[id]/targets/page.tsx`
- `src/features/employee-directory/components/employee-directory-listing.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
- `src/features/employee-directory/components/employee-directory-columns.tsx`
- `src/features/employee-directory/components/employee-directory-toolbar.tsx`
- `src/features/employee-directory/components/employee-directory-action-menu.tsx`
- `src/features/employee-directory/components/employee-directory-detail-page.tsx`
- `src/features/employee-directory/components/employee-target-form.tsx`
Employee Directory API/data layer:
- `src/app/api/employee-directory/route.ts`
- `src/app/api/employee-directory/[id]/target/route.ts`
- `src/features/employee-directory/api/types.ts`
- `src/features/employee-directory/api/service.ts`
- `src/features/employee-directory/api/queries.ts`
- `src/features/employee-directory/api/mutations.ts`
- `src/features/employee-directory/server/employee-directory-data.ts`
Employee CRUD support reused by directory:
- `src/app/api/employees/route.ts`
- `src/app/api/employees/[id]/route.ts`
- `src/features/employees/server/employee-data.ts`
- `src/features/employees/components/employee-form.tsx`
Shared search params:
- `src/lib/searchparams.ts`
## 3. UI Improvements
- Added page-level primary action: `สร้างรายชื่อพนักงาน`
- Added page-level secondary action: `Import รายชื่อพนักงาน`
- Reworked table columns into an HR-focused layout:
- รหัสพนักงาน
- ชื่อพนักงาน
- ฝ่าย
- ตำแหน่ง
- grouped training-hour columns
- action menu
- Added responsive search + filter toolbar
- Expanded action menu:
- ดูข้อมูลพนักงาน
- แก้ไขข้อมูลพนักงาน
- กำหนดเป้าหมาย K/S/A
- Improved detail page with:
- employee identity summary
- hour summary cards
- progress bar
- K/S/A summary cards
- training history table
## 4. Data Source Validation
Primary source for Employee Directory list/detail:
- `employees`
Related sources:
- `training_records`
- `employee_training_targets`
- `departments`
- `positions`
- `organizations`
Validation notes:
- list data does not depend on `users`
- employees without login accounts are still returned
- detail data resolves directly from employee master records
## 5. Training Hour Calculation
Training-hour summary is calculated from:
- `training_records.employee_id`
Current logic:
- Approved Hours:
- `approval_status = approved`
- uses `approved_hours`, fallback to submitted hours when applicable
- Pending Hours:
- `approval_status in (pending, needs_revision)`
- Rejected Hours:
- `approval_status = rejected`
- Total Hours:
- approved + pending + rejected
Employee target screen writes to:
- `employee_training_targets.employee_id`
## 6. Responsive Changes
- page header actions stack vertically on mobile and align horizontally on larger screens
- toolbar wraps cleanly on tablet
- table stays inside a horizontal scroll container
- detail page cards stack on smaller viewports
- edit/target actions on detail page wrap instead of overflowing
## 7. Permission Rules
Allowed:
- HRD/Admin can view employee directory
- HRD/Admin can create employee records
- HRD/Admin can edit employee records
- HRD/Admin can manage employee K/S/A targets
Blocked:
- Employee role cannot access Employee Directory by page guard
## 8. Manual Test Checklist
- [ ] Employee Directory reads from `employees`
- [ ] Employees without user account are displayed
- [ ] Search works by employee code, name, email, department, and position
- [ ] Company / department / position / status filters work
- [ ] `สร้างรายชื่อพนักงาน` opens the create page
- [ ] `Import รายชื่อพนักงาน` opens the import page
- [ ] Training hour calculation uses `training_records.employee_id`
- [ ] Employee detail page loads correctly
- [ ] Edit employee flow saves successfully
- [ ] K/S/A target page saves successfully
- [ ] Mobile/tablet layout does not overflow
## 9. Known Limitations
- Employee Directory filters currently use employee master text fields and related department/position names; there is no separate companies table yet.
- The K/S/A target editor is implemented as a dedicated page route, not an inline modal.
- Existing legacy compatibility paths in auth/training ownership were intentionally left in place from Sprint 6.12 migration work.
## Validation
TypeScript verification completed:
```bash
npx tsc --noEmit
```
## Migration Commands
No new database migration is required for this UI enhancement.

View File

@@ -0,0 +1,196 @@
# Sprint 6.12 Employee Ownership Final Migration Review
## 1. Summary
Sprint 6.12 completes the remaining runtime migration from user-owned training data to employee-owned training data in the main HRD paths.
Primary outcomes:
- Employee Directory now reads employee master data from `employees`.
- Dashboard overview aggregates training ownership from `training_records.employee_id`.
- Reports aggregate from `employees` and `training_records.employee_id`.
- Employee target resolution prefers `employee_training_targets.employee_id` and falls back to policy.
- Compatibility paths for login and legacy references remain intact.
## 2. Files Changed
Schema and migration:
- `src/db/schema.ts`
- `drizzle/0010_romantic_marauders.sql`
- `drizzle/meta/0010_snapshot.json`
- `drizzle/meta/_journal.json`
Employee identity and import:
- `src/features/employees/server/employee-identity-linking.ts`
- `src/features/employees/server/employee-data.ts`
- `src/app/api/import-employees/route.ts`
- `src/app/api/employees/route.ts`
Training records and ownership:
- `src/features/training-records/api/types.ts`
- `src/features/training-records/api/service.ts`
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-view-page.tsx`
- `src/features/training-records/server/training-record-data.ts`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/dashboard/training-records/[trainingRecordId]/page.tsx`
Employee directory:
- `src/features/employee-directory/server/employee-directory-data.ts`
- `src/features/employee-directory/components/employee-directory-listing.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
- `src/features/employee-directory/components/employee-directory-columns.tsx`
- `src/features/employee-directory/components/employee-directory-action-menu.tsx`
- `src/features/employee-directory/components/employee-directory-detail-page.tsx`
Overview and reports:
- `src/features/overview/server/overview-data.ts`
- `src/features/reports/server/report-data.ts`
- `src/features/reports/api/types.ts`
- `src/features/training-matrix/server/training-matrix-data.ts`
- `src/features/training-matrix/api/types.ts`
Training target resolution:
- `src/features/training-policy/server/employee-training-target-data.ts`
## 3. Employee Directory Data Source Validation
Implemented:
- List page now fetches from `employeesQueryOptions(...)`, not `usersQueryOptions(...)`.
- Table row shape now uses `Employee` contracts from the employee module.
- Row action routes now use `employeeId`.
- Detail page already resolves primary data from `employees`.
- Training summary in detail page uses `training_records.employee_id = employees.id`.
Validated behavior:
- Imported employees without linked user accounts remain visible in Employee Directory.
- Department and position display come from employee relations.
- Employee code and display fields come from employee-owned data, not auth-owned user data.
## 4. Overview Migration
Updated `src/features/overview/server/overview-data.ts` to:
- resolve self scope from `user_employee_maps` through `getEmployeeIdsForUser(...)`
- resolve the primary self employee through `getPrimaryEmployeeIdForUser(...)`
- use `employees` as the employee source for admin dashboards
- use `training_records.employee_id` for all yearly aggregates
- use employee-owned target resolution through `getResolvedTrainingTargetForEmployee(...)`
- use employee-owned target maps through `getResolvedTrainingTargetsForEmployees(...)`
Result:
- employee cards now represent the mapped employee record
- HRD cards, charts, rankings, and pending counts now use employee ownership consistently
## 5. Reports Migration
Updated `src/features/reports/server/report-data.ts` to:
- scope employees through `employees` instead of membership/user joins
- aggregate transcript rows from `employees + training_records.employee_id`
- count department employees from `employees`
- aggregate annual summary from employee-owned training records
- calculate training matrix from `employees`, `training_matrices`, and `training_records.employee_id`
- keep PDF and Excel export behavior working on the migrated datasets
Also updated report API types so report row ids now use employee ids as numbers.
## 6. Target Resolution Changes
Current target priority:
1. `employee_training_targets.employee_id`
2. `training_policies`
3. default zero-value fallback if no policy exists
Compatibility retained:
- user-based target resolution still exists only for legacy callers and fallback compatibility
- new migrated paths call employee-first target resolvers
## 7. Remaining Legacy Dependencies
Still required now:
- `training_records.user_id`
- retained for compatibility, notifications, and staged migration safety
- `employee_training_targets.user_id`
- retained for compatibility with legacy target readers/import history
- `users.employee_id`
- retained as fallback when no `user_employee_maps` row exists
- `users.employee_code`
- retained for compatibility and historical link recovery
Can be migrated later:
- legacy user-based target resolver branches in `employee-training-target-data.ts`
- compatibility updates in `employee-identity-linking.ts` that still backfill `trainingRecords.userId` and `employeeTrainingTargets.userId`
- user-centric fields in the users feature that still surface employee identity for admin maintenance
Safe candidates for future cleanup after validation period:
- direct runtime reads that rely on `training_records.user_id` for ownership semantics
- fallback reads from `users.employee_id` once all active accounts have `user_employee_maps`
- fallback reads from `users.employee_code` once mapping stability is confirmed
## 8. Regression Checklist
Code-level verification completed:
- `npx tsc --noEmit`
Expected runtime validation checklist:
- Employee login
- Employee dashboard
- HRD dashboard
- Employee Directory list
- Employee Directory detail
- Employee imported without user appears in Employee Directory
- Employee transcript
- Department summary
- Annual summary
- Training matrix
- Training record create/edit
- HRD review
- Employee import
- Employee target import
## 9. Risks
- Existing databases require the new migration before the new ownership fields and map table are fully available.
- Some compatibility code still writes both employee-owned and user-owned references; that is intentional for this sprint, but it increases temporary complexity.
- If older rows lack employee linkage, self-scoped dashboards and reports will return empty data until mapping is backfilled.
## 10. Recommendation Before Sprint 7.2
- Run the new Drizzle migration in every active environment.
- Backfill and verify `user_employee_maps` coverage for all active employee users.
- Run UAT on dashboard, directory, reports, training record create/review, and import flows using real migrated data.
- Keep legacy user-owned columns for one more validation cycle, then schedule a dedicated cleanup sprint.
## Migration Commands
Run the pending migration:
```bash
npm run migrate
```
Optional validation:
```bash
npx tsc --noEmit
```

View File

@@ -0,0 +1,199 @@
# Sprint 6.5 Pre-Sprint 7 Hardening Review
## 1. Summary
Sprint 6.5 focused on hardening work only, without adding new business features or changing the current database schema.
This round completed four targeted improvements:
- moved notification pagination and type filtering to SQL/Drizzle query level
- added route-level loading and error coverage for announcement pages
- updated RBAC documentation to match the current Auth.js and server-side authorization model
- created a browser smoke test checklist for Chrome and Microsoft Edge before Sprint 7
The changes were intentionally small in scope and kept existing routes, Auth.js, Drizzle ORM, Thai UI, and current responsive behavior intact.
## 2. Files Changed
- [src/features/notifications/server/notification-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/server/notification-data.ts)
- [src/app/dashboard/announcements/loading.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/loading.tsx)
- [src/app/dashboard/announcements/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/error.tsx)
- [src/app/dashboard/announcements/[id]/loading.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/[id]/loading.tsx)
- [src/app/dashboard/announcements/[id]/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/[id]/error.tsx)
- [docs/nav-rbac.md](/d:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md)
## 3. Issues Fixed
### Fixed 1: Notification pagination was inefficient
Previous behavior:
- loaded all matching notifications
- filtered notification type in memory
- sliced the result in memory for pagination
Current behavior:
- applies ownership, status, search, and type filtering in SQL
- uses `WHERE`, `ORDER BY`, `LIMIT`, and `OFFSET`
- preserves unread count behavior
- keeps the same API contract for notification center and header popover
### Fixed 2: Announcement routes lacked route-level loading and error states
Added route-segment coverage for:
- `/dashboard/announcements`
- `/dashboard/announcements/[id]`
### Fixed 3: RBAC documentation was outdated
The old document referenced Clerk and client-side authorization assumptions that no longer match the repository.
The document now reflects:
- Auth.js
- `proxy.ts`
- `requireOrganizationAccess()`
- `requireHRD()`
- `requireEmployeeDashboardAccess()`
- direct URL protection
- API protection
- protected file download behavior
## 4. Notification Pagination Optimization
Implementation summary:
- replaced in-memory type filtering with SQL clauses
- kept organization and user ownership checks
- preserved:
- search behavior
- read/unread filtering
- notification type filtering
- unread badge accuracy
- computed `total_notifications` from SQL count instead of filtered array length
Primary file:
- [src/features/notifications/server/notification-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/server/notification-data.ts)
Behavior impact:
- no API route changes required
- no frontend service contract changes required
- notification center page and header notification popover continue to use the same endpoint
## 5. Announcement Loading/Error Coverage
Added route-level files:
- [src/app/dashboard/announcements/loading.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/loading.tsx)
- [src/app/dashboard/announcements/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/error.tsx)
- [src/app/dashboard/announcements/[id]/loading.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/[id]/loading.tsx)
- [src/app/dashboard/announcements/[id]/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/[id]/error.tsx)
These follow the existing dashboard pattern:
- Thai UI text
- `PageContainer`
- skeleton loading state
- resettable route-level error state
- responsive layout
## 6. RBAC Documentation Update
Updated file:
- [docs/nav-rbac.md](/d:/WY-2569/HRD/training-system-minimal-refactor/docs/nav-rbac.md)
The document now explains:
- navigation filtering is UX only
- Auth.js is the authentication source
- proxy-level route protection
- server-side page guards
- organization-based access rules
- employee vs HRD access model
- API protection model
- direct URL protection
- protected file download routes
## 7. Browser Smoke Test Checklist
Target browsers:
- Chrome
- Microsoft Edge
### Employee checklist
1. Sign in with employee account
2. Open dashboard overview and confirm data loads
3. Create a new training record
4. Upload certificate file
5. Open certificate preview and download/open file
6. Edit a pending training record
7. Delete a pending training record
8. Open notification center and verify notification detail link works
9. Open announcement list and announcement detail
### HRD checklist
1. Sign in with HRD account
2. Open dashboard overview and confirm data loads
3. Open pending review queue
4. Approve a training record
5. Reject a training record
6. Create an announcement
7. Open or download announcement attachment
8. Import employee Excel file
9. Approve and reject pending master review items
10. Open audit logs page
11. Export audit log CSV
### Cross-check expectations
1. No unauthorized page should open by direct URL
2. Employee should not see HRD-only data
3. HRD should be able to access organization-wide review flows
4. Protected certificate and announcement files should open only for authorized users
5. Loading and error states should render cleanly on announcement routes
## 8. Validation Result
Validation completed:
- `npx tsc --noEmit` passed
- `npm run lint` passed with existing non-blocking warnings in unrelated files
- `npm run build` passed
Database:
- no migration required
- no table or column changes made
Routes:
- existing routes remained intact
- production build confirmed these routes compile successfully, including:
- `/api/notifications`
- `/dashboard/notifications`
- `/dashboard/announcements`
- `/dashboard/announcements/[id]`
## 9. Remaining Risks
1. Browser smoke testing has been documented but not fully executed end-to-end in this hardening task
2. Existing unrelated lint warnings remain in legacy or shared UI files outside this scope
3. Protected file delivery still uses local app storage and process memory, which is acceptable for current scale but should be revisited later if file volume grows
## 10. Sprint 7 Readiness Score
- Sprint 7 Readiness Score: **91/100**
Readiness interpretation:
- the repo is in a stronger state for Sprint 7 development
- the main pre-UAT hardening gaps from Sprint 6 review are addressed
- a short manual smoke pass in Chrome and Edge is still recommended before wider business testing

View File

@@ -0,0 +1,134 @@
# Sprint 6 Phase 1 Audit Log Review
## Summary
ดำเนินการสร้าง Audit Log Foundation ตามขอบเขต Phase 1 แล้ว โดยใช้ตาราง `audit_logs` เดิมใน schema ปัจจุบัน ไม่ได้เปลี่ยนโครงสร้างฐานข้อมูล และต่อเข้ากับ Auth.js, Route Handlers, React Query, UI table และ RBAC เดิมของระบบ
สิ่งที่ทำหลัก ๆ:
- สร้างโมดูล `audit-logs` สำหรับ query, service, server formatter, และ UI table
- สร้างหน้า `/dashboard/audit-logs`
- สร้าง API:
- `GET /api/audit-logs`
- `GET /api/audit-logs/export`
- เพิ่ม reusable audit service: `logAuditEvent(...)`
- ฝัง audit event ใน authentication และ route หลักตามที่ sprint ระบุ
- จำกัดสิทธิ์หน้าและ API ให้ HRD เท่านั้น
- เพิ่ม loading, empty, error state และ CSV export
## Files Changed
- `src/app/dashboard/audit-logs/page.tsx`
- `src/app/api/audit-logs/route.ts`
- `src/app/api/audit-logs/export/route.ts`
- `src/app/api/training-records/route.ts`
- `src/app/api/training-records/[id]/route.ts`
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/api/courses/route.ts`
- `src/app/api/courses/[id]/route.ts`
- `src/app/api/training-policies/route.ts`
- `src/app/api/training-policies/[id]/route.ts`
- `src/app/api/import-employees/route.ts`
- `src/app/api/master-review/route.ts`
- `src/auth.ts`
- `src/proxy.ts`
- `src/lib/searchparams.ts`
- `src/features/audit-logs/api/types.ts`
- `src/features/audit-logs/api/service.ts`
- `src/features/audit-logs/api/queries.ts`
- `src/features/audit-logs/constants/audit-log-options.ts`
- `src/features/audit-logs/schemas/audit-log.ts`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/audit-logs/server/audit-log-data.ts`
- `src/features/audit-logs/components/audit-log-listing.tsx`
- `src/features/audit-logs/components/audit-log-table.tsx`
- `src/features/audit-logs/components/audit-log-toolbar.tsx`
- `src/features/audit-logs/components/audit-log-columns.tsx`
## Database Changes
- ไม่มีการเปลี่ยน schema
- ไม่มี migration ใหม่
- ใช้งานตารางเดิม `audit_logs` จาก `src/db/schema.ts`
## Events Implemented
### Authentication
- `LOGIN_SUCCESS`
- `LOGIN_FAILED`
### Training Records
- `CREATE`
- `UPDATE`
- `DELETE`
### HRD Review
- `APPROVE`
- `REJECT`
### Course Master
- `CREATE`
- `UPDATE`
### Training Policy
- `CREATE`
- `UPDATE`
### Employee Import
- `IMPORT`
### Master Review
- `APPROVE`
- `REJECT`
## Permission Rules
- หน้า `/dashboard/audit-logs` ใช้ `requireHRDDashboardAccess()`
- API `/api/audit-logs` และ `/api/audit-logs/export` ใช้ `requireHRD()`
- เพิ่ม `/api/audit-logs` ใน `src/proxy.ts` เพื่อบังคับ auth ระดับ proxy
- ผู้ใช้ทั่วไปไม่สามารถเข้าดู audit log ได้
## Manual Test Checklist
- เข้าระบบด้วยบัญชีถูกต้อง แล้วตรวจสอบว่ามี `LOGIN_SUCCESS`
- เข้าระบบด้วยรหัสผ่านผิด แล้วตรวจสอบว่ามี `LOGIN_FAILED`
- สร้าง training record แล้วตรวจสอบ event `TRAINING_RECORDS / CREATE`
- แก้ไข training record สถานะ pending แล้วตรวจสอบ event `TRAINING_RECORDS / UPDATE`
- ลบ training record สถานะ pending แล้วตรวจสอบ event `TRAINING_RECORDS / DELETE`
- HRD อนุมัติ record แล้วตรวจสอบ event `HRD_REVIEW / APPROVE`
- HRD ปฏิเสธ record แล้วตรวจสอบ event `HRD_REVIEW / REJECT`
- สร้าง course แล้วตรวจสอบ event `COURSE_MASTER / CREATE`
- แก้ไข course แล้วตรวจสอบ event `COURSE_MASTER / UPDATE`
- สร้าง training policy แล้วตรวจสอบ event `TRAINING_POLICY / CREATE`
- แก้ไข training policy แล้วตรวจสอบ event `TRAINING_POLICY / UPDATE`
- import employee file แล้วตรวจสอบ event `EMPLOYEE_IMPORT / IMPORT`
- approve/reject master review แล้วตรวจสอบ event `MASTER_REVIEW / APPROVE` หรือ `REJECT`
- เปิดหน้า `/dashboard/audit-logs` แล้วทดสอบ:
- ค้นหา
- filter ผู้ใช้งาน
- filter โมดูล
- filter รายการ
- filter วันที่
- export CSV
## Known Limitations
- Login failed ที่ยังผูก organization ไม่ได้ จะไม่แสดงในมุมมององค์กรนั้น เพราะหน้า audit log เป็น organization-scoped
- CSV export จำกัดที่ 1000 แถวต่อครั้งใน implementation ปัจจุบัน
- Search ปัจจุบันเน้นค้นจาก user/module/action/entity ไม่ได้ full-text ภายใน JSON snapshot ทั้งหมด
- ยังไม่ได้เพิ่ม audit event ให้ module อื่นนอกขอบเขต sprint นี้
## Next Phase Recommendation
- เพิ่ม date range picker แบบใช้งานสะดวกขึ้นแทน input date ธรรมดา
- เพิ่ม advanced search บน `old_value` และ `new_value`
- เพิ่ม pagination/export แบบ background job หาก log โตเกินระดับ 1000+ แถว
- เพิ่ม audit coverage ให้ notifications, announcements, users, training matrix และ reports export
- เพิ่ม retention policy หรือ archive strategy หากต้องเก็บ log ระยะยาว

View File

@@ -0,0 +1,134 @@
# Sprint 6 Phase 2 Notification Review
## Summary
ดำเนินการสร้าง Notification Center แบบใช้งานจริงบนฐานข้อมูลเดิมเรียบร้อยแล้ว โดยใช้ตาราง `notifications` ที่มีอยู่แล้วในระบบ และเชื่อมเข้ากับ Auth.js, Drizzle, React Query, header notification popover และหน้า `/dashboard/notifications`
งานในรอบนี้ครอบคลุม:
- สร้าง notification data layer และ reusable notification service
- เพิ่ม API สำหรับ list, mark read, mark all read
- เชื่อม notification event เข้ากับ training review, employee import และ master review
- เปลี่ยน notification center/header จาก mock store เป็นข้อมูลจริงจากฐานข้อมูล
- สร้าง notification center page แบบค้นหา กรอง อ่านทีละรายการ และอ่านทั้งหมด
- เพิ่ม audit log สำหรับ mark read และ mark all read
## Files Changed
- `src/app/dashboard/notifications/page.tsx`
- `src/app/api/notifications/route.ts`
- `src/app/api/notifications/[id]/read/route.ts`
- `src/app/api/notifications/read-all/route.ts`
- `src/app/api/training-records/[id]/review/route.ts`
- `src/app/api/import-employees/route.ts`
- `src/app/api/master-review/route.ts`
- `src/proxy.ts`
- `src/components/ui/notification-card.tsx`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/audit-logs/constants/audit-log-options.ts`
- `src/features/notifications/api/types.ts`
- `src/features/notifications/api/service.ts`
- `src/features/notifications/api/queries.ts`
- `src/features/notifications/api/mutations.ts`
- `src/features/notifications/constants/notification-options.ts`
- `src/features/notifications/server/notification-service.ts`
- `src/features/notifications/server/notification-data.ts`
- `src/features/notifications/components/notifications-listing.tsx`
- `src/features/notifications/components/notifications-page.tsx`
- `src/features/notifications/components/notification-center.tsx`
## Database Changes
- ไม่มีการเปลี่ยน schema
- ไม่มี migration ใหม่
- ใช้งานตารางเดิม `notifications`
## Routes Added
- `GET /api/notifications`
- `PATCH /api/notifications/[id]/read`
- `PATCH /api/notifications/read-all`
- ใช้งานหน้าเดิม `/dashboard/notifications` แต่เปลี่ยนเป็น data-driven implementation
## Notification Events Implemented
- `createNotification()`
- `createTrainingApprovedNotification()`
- `createTrainingRejectedNotification()`
- `createAnnouncementNotification()`
- `createImportCompletedNotification()`
- `createMasterReviewNotification()`
- `markNotificationAsRead()`
- `markAllNotificationsAsRead()`
Workflow ที่เชื่อมแล้ว:
- HRD อนุมัติ training record -> แจ้ง employee
- HRD ปฏิเสธ training record -> แจ้ง employee
- Employee import completed -> แจ้ง HRD ผู้ที่ทำรายการ
- Master review approve/reject -> แจ้ง HRD ผู้ที่ทำรายการ
## Permission Rules
- ผู้ใช้เห็นเฉพาะ notification ของตัวเอง
- HRD เห็นเฉพาะ notification ของตัวเอง
- API บังคับ ownership ที่ server-side ด้วย `organizationId + userId`
- การอ่าน notification ทีละรายการและอ่านทั้งหมดทำได้เฉพาะของตัวเอง
- หน้า `/dashboard/notifications` ใช้ `requireEmployeeDashboardAccess()`
- API `/api/notifications/**` ใช้ `requireOrganizationAccess()`
## Audit Events Added
- `NOTIFICATION_MARK_READ`
- `NOTIFICATION_MARK_ALL_READ`
- `NOTIFICATION_CREATE` สำหรับ system/manual notification flow ที่เปิด audit
หมายเหตุ:
- ไม่ได้ audit notification อัตโนมัติทุกตัว เพื่อลด log noise
- mark read actions ถูก audit ตาม requirement
## Thai Localization Changes
- เปลี่ยน notification center และ notification page เป็นข้อความไทย
- ปรับ label ของ filter, action, state, empty/error/loading ให้เป็นไทย
- ปรับ relative time บน notification card เป็นไทย
## Responsive/Mobile Notes
- Notification popover ยังคงรองรับ mobile width แบบเดิม
- หน้า `/dashboard/notifications` ใช้ stacked filters บนจอเล็ก
- ปุ่ม action และ mark-all ยังใช้งานได้บน mobile
- card layout ไม่ซ้อนทับข้อความและยังรองรับหลายบรรทัด
## Manual Test Checklist
- Employee can see only own notifications.
- HRD can see only own notifications.
- Training approved creates notification for employee.
- Training rejected creates notification for employee.
- Import completed creates notification for HRD.
- Master review action creates notification for HRD.
- Unread count displays correctly.
- Mark as read works.
- Mark all as read works.
- Search works.
- Filter unread/read works.
- Notification center works on mobile.
- Direct API access cannot read other users' notifications.
- Audit log records mark read actions.
## Known Limitations
- ตาราง `notifications.type` เดิมเป็น enum ชุดเก่า (`approved`, `rejected`, `announcement`, `reminder`) จึงต้อง map logical notification type ของ phase นี้เข้ากับ enum เดิมแทนการเพิ่ม enum ใหม่
- filter ประเภท notification ฝั่ง server ใช้การ resolve จาก `type + referenceType` หลัง query ไม่ได้ทำ SQL-native classification
- `createAnnouncementNotification()` ถูกเตรียมไว้แล้ว แต่ announcements module ยังเป็น placeholder จึงยังไม่ได้เชื่อม workflow publish จริงใน phase นี้
- notification center popover แสดงล่าสุด 5 รายการ ส่วนหน้ารวมใช้ pagination
## Next Phase Recommendation
- เชื่อม announcements publish workflow ให้สร้าง notification จริง
- เพิ่ม background delivery strategy หากภายหลังต้องรองรับ email/push
- เพิ่ม bulk cleanup หรือ archive strategy สำหรับ notification จำนวนมาก
- เพิ่ม richer notification linking เช่น deep link ไปหน้ารายละเอียดแบบ anchor/state-aware
- พิจารณาปรับ enum notification type ใน DB เมื่อพร้อมทำ migration อย่างเป็นทางการ

View File

@@ -0,0 +1,94 @@
# Sprint 6 Phase 3 Announcement Review
## 1. Summary
เพิ่ม Announcement Module ครบทั้งฝั่ง HRD และ Employee โดยเชื่อมกับ Auth.js, Drizzle, Notification Service และ Audit Log Service ตาม architecture เดิมของระบบ
## 2. Files Changed
- `src/app/api/announcements/route.ts`
- `src/app/api/announcements/[id]/route.ts`
- `src/app/dashboard/announcements/page.tsx`
- `src/app/dashboard/announcements/[id]/page.tsx`
- `src/features/announcements/api/types.ts`
- `src/features/announcements/api/service.ts`
- `src/features/announcements/api/queries.ts`
- `src/features/announcements/api/mutations.ts`
- `src/features/announcements/schemas/announcement.ts`
- `src/features/announcements/server/announcement-data.ts`
- `src/features/announcements/components/announcements-listing.tsx`
- `src/features/announcements/components/announcements-page.tsx`
- `src/features/announcements/components/announcement-view-page.tsx`
- `src/features/announcements/components/announcement-form.tsx`
- `src/features/audit-logs/server/audit-service.ts`
- `src/features/audit-logs/constants/audit-log-options.ts`
- `src/features/notifications/server/notification-data.ts`
- `src/lib/announcement-storage.ts`
- `src/lib/searchparams.ts`
- `src/proxy.ts`
## 3. Database Changes
ไม่มีการเปลี่ยน schema และไม่มี migration เพิ่ม ใช้งานตาราง `announcements` ที่มีอยู่แล้ว
## 4. Routes Added
- `GET /api/announcements`
- `POST /api/announcements`
- `GET /api/announcements/[id]`
- `PATCH /api/announcements/[id]`
- `/dashboard/announcements`
- `/dashboard/announcements/[id]`
## 5. Notification Integration
- เมื่อประกาศถูกเผยแพร่ ระบบจะเรียก `createAnnouncementNotification()`
- Notification เก็บ `referenceType = announcement` และ `referenceId = announcement.id`
- ปรับปลายทาง notification ให้เปิดไปยังหน้ารายละเอียดประกาศโดยตรง
## 6. Audit Events Added
- `ANNOUNCEMENT_CREATE`
- `ANNOUNCEMENT_UPDATE`
- `ANNOUNCEMENT_PUBLISH`
- `ANNOUNCEMENT_ARCHIVE`
## 7. Permission Rules
- HRD: สร้าง แก้ไข เผยแพร่ ปักหมุด และเก็บถาวรได้
- Employee: ดูได้เฉพาะประกาศที่ `published` และอยู่ในช่วงวันที่อนุญาตให้แสดง
- การบังคับสิทธิ์ทำที่ server route
## 8. Responsive Notes
- หน้า list ใช้ card layout รองรับ mobile และ tablet
- filter bar ยุบเป็นแนวตั้งบนจอเล็ก
- action buttons และ pagination รองรับการใช้งานบนหน้าจอแคบ
## 9. Thai Localization Notes
- ข้อความหลักในหน้า list, detail, form, loading, empty, error และ toast เป็นภาษาไทย
## 10. Manual Test Checklist
- HRD เปิด `/dashboard/announcements` แล้วเห็นรายการทั้งหมด
- HRD สร้างประกาศแบบ draft ได้
- HRD สร้างประกาศแบบ published ได้และเกิด notification
- HRD แก้ไขประกาศและแทนที่ไฟล์แนบได้
- HRD ปักหมุดและยกเลิกปักหมุดได้
- HRD เก็บถาวรประกาศได้
- Employee เห็นเฉพาะประกาศที่เผยแพร่แล้ว
- Employee เปิด notification แล้วเข้าหน้ารายละเอียดประกาศได้
- ค้นหา กรอง และเปลี่ยนหน้าใน announcement list ได้
## 11. Known Limitations
- ยังไม่มี rich text editor สำหรับ content
- attachment upload ใช้ local file storage ตามแนวทางเดียวกับ certificate
- ยังไม่มี route ยกเลิกเผยแพร่ถาวร เพราะ requirement ไม่ได้ระบุ
## 12. Next Phase Recommendation
- เพิ่ม announcement analytics เช่นยอดอ่านหรือ acknowledgement
- เพิ่ม scheduling UX สำหรับประกาศที่จะเผยแพร่ล่วงหน้า
- พิจารณาเพิ่ม editor ที่รองรับรูปแบบข้อความ หากมี requirement ด้าน communication มากขึ้น

View File

@@ -0,0 +1,119 @@
# Sprint 6 Phase 4 Dashboard Review
## 1. Summary
ปรับปรุง Dashboard ทั้งฝั่ง Employee และ HRD บนโมดูล `overview` เดิม โดยเพิ่ม KPI cards, filter, chart sections, K/S/A progress, recent activity และย้ายการคำนวณหลักไปใช้ database aggregation เพื่อลดการโหลดข้อมูลดิบเข้าหน่วยความจำ
## 2. Files Changed
- `src/app/dashboard/overview/layout.tsx`
- `src/app/dashboard/overview/page.tsx`
- `src/app/dashboard/overview/@area_stats/page.tsx`
- `src/app/dashboard/overview/@bar_stats/page.tsx`
- `src/app/dashboard/overview/@pie_stats/page.tsx`
- `src/app/dashboard/overview/@sales/page.tsx`
- `src/features/overview/server/overview-data.ts`
- `src/features/overview/components/overview-filter-panel.tsx`
- `src/features/overview/components/ksa-progress.tsx`
- `src/features/overview/components/top-courses-chart.tsx`
- `src/features/overview/components/area-graph.tsx`
- `src/features/overview/components/bar-graph.tsx`
- `src/features/overview/components/pie-graph.tsx`
## 3. Database Queries Added
- Aggregate KPI query by approval status
- Aggregate K/S/A approved hours by category
- Monthly approved-hours and approved-record trend by month
- Department ranking by approved hours
- Top courses by approved hours
- Active employee scope query with company/department filters
- Passed / not-passed target calculation from per-user yearly approved hours
- Pending review count query
- Distinct filter option queries for year, company, department
- Recent activity query with `limit 5`
## 4. KPI Cards Added
### Employee
- Approved Hours
- Pending Hours
- Rejected Hours
- Completion Percentage
### HRD
- Total Employees
- Employees Passed Target
- Employees Not Passed Target
- Pending Review Count
## 5. Charts Added
### Employee
- K/S/A Progress section
- Recent Activity section
### HRD
- K/S/A Distribution
- Monthly Training Trend
- Department Ranking
- Top Courses
## 6. Filters Added
### Employee
- Year
### HRD
- Year
- Company
- Department
## 7. Responsive Notes
- Filter panel ใช้ grid ที่ยุบลงได้บน mobile/tablet
- KPI cards แสดง 1 คอลัมน์บนจอแคบ และขยายเป็น 2/4 คอลัมน์ตาม breakpoint
- Employee dashboard ใช้ 2 sections หลักที่ไม่บีบข้อความจนอ่านยากบน 360px - 412px
- HRD charts ถูกจัดใน grid เดิมของ overview และยังคงรองรับ 768px, 1024px, 1440px
## 8. Performance Notes
- ใช้ SQL aggregation สำหรับ KPI และ charts แทนการโหลด training records ทั้งหมดมา reduce ใน memory
- Recent activity จำกัดที่ 5 รายการ
- Ranking charts จำกัดที่ top 5
- Filter options ดึงจาก query ที่ group/distinct ตาม organization
- ใช้ `cache()` เพื่อให้ layout และ parallel routes ใช้ dataset ชุดเดียวกันใน request เดียว
## 9. Manual Test Checklist
- Employee เปิด `/dashboard/overview` แล้วเห็น KPI 4 ใบ
- Employee เปลี่ยนปีแล้ว KPI, K/S/A progress และ recent activity เปลี่ยนตาม
- HRD เปิด `/dashboard/overview` แล้วเห็น KPI 4 ใบและ chart 4 ส่วน
- HRD กรองตามปี / บริษัท / แผนก แล้ว KPI และ chart เปลี่ยนตาม
- HRD ดู Pending Review Count ได้ถูกต้อง
- Recent Activity แสดงสถานะรายการอบรมถูกต้อง
- Dashboard ใช้งานได้บน 360px, 390px, 412px, 768px, 1024px, 1440px
## 10. Known Limitations
- การกรอง HRD ตาม company/department ยังอิงข้อมูลบน `users` ปัจจุบัน ไม่ใช่ historical snapshot ตอน submit record
- Top Courses และ Department Ranking ใช้ approved hours เป็นตัวจัดอันดับหลัก
- หากปีที่เลือกไม่มี training policy ระบบจะ fallback ไป policy ล่าสุด หรือ 0 ชั่วโมงเมื่อไม่มี policy เลย
## 11. Go-Live Readiness Score
**8.5 / 10**
เหตุผล:
- ฟังก์ชันหลักครบตาม requirement
- ไม่มี schema change และไม่ต้อง migrate
- typecheck ผ่าน
- lint ผ่าน โดยเหลือเฉพาะ warning เดิมของโปรเจกต์ที่ไม่เกี่ยวกับ phase นี้
- ยังควรมี manual QA ฝั่ง responsive และตรวจค่าจริงกับ dataset production-like ก่อน go-live

View File

@@ -0,0 +1,172 @@
# Sprint 7 Phase 1 Reports Review
## Summary
Sprint 7 Phase 1 was implemented on top of the existing reports module without changing the project structure or ORM setup.
This phase adds four report views:
1. Employee Training Transcript
2. Training Matrix
3. Department Summary
4. Annual Training Summary
The implementation now uses database-side aggregation for summary-style reports and keeps employee access scoped to their own transcript through the existing Auth.js organization session.
No database migration was required.
## Files Changed
- `src/app/dashboard/reports/page.tsx`
- `src/app/api/reports/export/route.ts`
- `src/config/nav-config.ts`
- `src/features/reports/api/types.ts`
- `src/features/reports/components/report-table-card.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/server/report-data.ts`
- `src/assets/fonts/THSarabunNew.ttf`
- `package.json`
- `package-lock.json`
## Database Queries Added
The reports layer now uses SQL and Drizzle-based aggregation instead of relying on in-memory report assembly for the Sprint 7 report set.
Added query patterns:
1. Employee transcript query filtered by:
- organization
- year
- company
- department
- current employee scope for non-HRD users
2. Department summary aggregation:
- employee count per department
- training record count per department
- approved / pending / rejected counts
- approved hour totals
3. Annual training summary aggregation:
- grouped by month
- submitted / approved / pending / rejected counts
- submitted hours
- approved hours
4. Training matrix compliance SQL:
- employee scope CTE
- required assignment CTE
- latest approved record CTE
- final compliance summary per employee
## Reports Added
### 1. Employee Training Transcript
- Available to all authenticated employees with organization access
- Employees see only their own records
- HRD/admin can see organization-wide results
### 2. Training Matrix
- HRD/admin only
- Shows required courses, completed courses, compliance percentage, and missing courses
### 3. Department Summary
- HRD/admin only
- Shows employee totals, record totals, status totals, and approved hours by department
### 4. Annual Training Summary
- HRD/admin only
- Shows monthly reporting and approval totals for the selected year
## Export Features
Added per-report export actions for:
- Excel (`.xlsx`)
- PDF (`.pdf`)
Implementation details:
- Excel export uses `xlsx`
- PDF export uses `pdf-lib`
- Thai PDF output uses embedded font file:
- `src/assets/fonts/THSarabunNew.ttf`
Supported export route:
- `/api/reports/export`
Supported query params:
- `report`
- `format`
- `year`
- `company`
- `departmentId`
## Permission Rules
### Employee
- Can open `/dashboard/reports`
- Can view only `Employee Training Transcript`
- Can export only `Employee Training Transcript`
### HRD/Admin
- Can open `/dashboard/reports`
- Can view all four reports organization-wide
- Can export all four reports
### Server-side Enforcement
- Export route validates report type and format
- Export route blocks non-HRD users from exporting admin-only reports
- Dataset queries are scoped by current organization and session user
## Responsive Notes
- Filter area uses responsive grid layout
- Export actions stack on small screens and align horizontally on larger screens
- Report tables remain horizontally scrollable on mobile/tablet
- Summary cards collapse cleanly from 4 columns down to smaller breakpoints
## Manual Test Checklist
1. Sign in as employee and open `/dashboard/reports`
2. Confirm only transcript report is visible
3. Confirm transcript data contains only the current employee records
4. Export employee transcript to Excel
5. Export employee transcript to PDF
6. Sign in as HRD/admin and open `/dashboard/reports`
7. Filter by year and verify transcript, matrix, department summary, and annual summary update
8. Filter by company and verify results update
9. Filter by department and verify results update
10. Export each report to Excel
11. Export each report to PDF
12. Confirm sidebar shows Reports for employee and HRD roles
13. Confirm direct export access to admin-only reports returns forbidden for employee role
## Known Limitations
1. PDF export is formatted as a lightweight text/table document, not a branded print layout.
2. The Training Matrix report depends on current matrix assignments and latest approved records before the selected year end.
3. Lint still reports pre-existing warnings in unrelated files outside this Sprint 7 scope.
## Sprint 7.2 Recommendation
Recommended next improvements:
1. Add chart-based visualization for annual and department summaries
2. Add background export jobs if report volume grows significantly
3. Add report presets and saved filters
4. Add richer PDF layout with pagination headers, footer, and branding
5. Consider moving the reports page to query-backed client filters only if UX requires instant filter changes
## Migration Commands
No migration commands are required for Sprint 7 Phase 1.

89
docs/test-data-guide.md Normal file
View File

@@ -0,0 +1,89 @@
# Test Data Guide
## 1. Persona Data
ควรเตรียมผู้ใช้ขั้นต่ำดังนี้:
| Persona | จำนวน | จุดประสงค์ |
| ----------- | ----- | ------------------------------------------------------- |
| Super Admin | 1 | organizer management, permission management, audit logs |
| HRD Admin | 1-2 | master data, training workflow, reports, import |
| Employee | 2-3 | self-service flow, scope isolation, notification |
## 2. Organization Data
- Organization A สำหรับการทดสอบหลัก
- Organization B สำหรับทดสอบ cross-organization isolation
## 3. Master Data
ควรมีข้อมูลขั้นต่ำ:
- 2 companies
- 2 departments
- 2 positions
- 3 courses
- 1 active training policy
- 2 training matrix rules
## 4. Training Record Data
ควรมีอย่างน้อย:
- 1 draft record
- 1 pending record
- 1 approved record
- 1 needs_revision record
- 1 rejected record
- 1 record ที่มี certificate
## 5. Announcement Data
- 1 draft announcement
- 1 published announcement ที่ active
- 1 archived announcement
- 1 announcement ที่มี attachment
- 1 announcement ที่ end date ผ่านแล้ว
## 6. Online Lesson Data
- 1 draft lesson
- 1 published lesson แบบ video URL
- 1 published lesson แบบ uploaded file
- 1 archived lesson
- 1 lesson ที่มี attachment และ thumbnail
## 7. Reports Data
เพื่อให้ report มีความหมาย ควรมี:
- training records หลายเดือนในปีเดียวกัน
- พนักงานหลายแผนก
- approved hours หลายระดับ
- target hours จากทั้ง employee target และ training policy
## 8. Import / Master Review Data
ควรเตรียมไฟล์ import 3 แบบ:
- ไฟล์ valid ทั้งหมด
- ไฟล์มีบางแถว error
- ไฟล์ที่สร้าง pending company / department / position
## 9. Permission Management Data
- 1 system template
- 2 non-system templates
- 1 user ที่ยังไม่ assigned template
- 1 user ที่ assigned template แล้ว
- 1 user ที่มี allow override
- 1 user ที่มี deny override
## 10. Negative Test Data
- email ซ้ำ
- employee code ซ้ำ
- ไฟล์ผิดประเภท
- ไฟล์เกินขนาด
- query string ที่พยายามเปิดข้อมูลของคนอื่น
- organization B data ที่ user organization A ไม่ควรเห็น

595
docs/themes.md Normal file
View File

@@ -0,0 +1,595 @@
# Adding New Themes
This guide explains how to add a new theme to the application. The theme system uses CSS custom properties with `[data-theme]` selectors for easy theme switching.
## The Journey: Adding a New Theme
When adding a new theme, follow this journey:
1. **Create theme CSS file**`src/styles/themes/your-theme-name.css` with `[data-theme='your-theme-name']`
2. **Import theme** → Add `@import` to `src/styles/theme.css`
3. **Register theme** → Add to `THEMES` array in `src/components/themes/theme.config.ts`
4. **Add fonts (if needed)** → Import fonts in `src/components/themes/font.config.ts` if using custom Google Fonts
5. **Set as default (optional)** → Update `DEFAULT_THEME` in `src/components/themes/active-theme.tsx`
See the **Step-by-Step Guide** section below for detailed instructions.
## Quick Start: Set Your Theme as Default
To make your new theme the default (so it loads automatically without the theme switcher):
1. Open `src/components/themes/active-theme.tsx`
2. Change line 12: `const DEFAULT_THEME = 'your-theme-name';`
3. Save and restart your dev server
That's it! Your theme will now be the default for all new users.
> **Note:** Make sure you've completed steps 1-3 above before setting a theme as default.
## Theme Structure
All themes are located in `src/styles/themes/` directory. Each theme is a complete, self-contained CSS file that defines all design tokens for both light and dark modes.
## File Format
Each theme file must follow this structure:
```css
/* Light mode tokens */
[data-theme='your-theme-name'] {
/* Color tokens */
--background: oklch(...);
--foreground: oklch(...);
--card: oklch(...);
--card-foreground: oklch(...);
--popover: oklch(...);
--popover-foreground: oklch(...);
--primary: oklch(...);
--primary-foreground: oklch(...);
--secondary: oklch(...);
--secondary-foreground: oklch(...);
--muted: oklch(...);
--muted-foreground: oklch(...);
--accent: oklch(...);
--accent-foreground: oklch(...);
--destructive: oklch(...);
--destructive-foreground: oklch(...);
--border: oklch(...);
--input: oklch(...);
--ring: oklch(...);
/* Chart colors */
--chart-1: oklch(...);
--chart-2: oklch(...);
--chart-3: oklch(...);
--chart-4: oklch(...);
--chart-5: oklch(...);
/* Sidebar colors */
--sidebar: oklch(...);
--sidebar-foreground: oklch(...);
--sidebar-primary: oklch(...);
--sidebar-primary-foreground: oklch(...);
--sidebar-accent: oklch(...);
--sidebar-accent-foreground: oklch(...);
--sidebar-border: oklch(...);
--sidebar-ring: oklch(...);
/* Typography */
/* Option 1: Use fonts from next/font/google (recommended) */
--font-sans: 'Font Name', sans-serif; /* Use the font's display name */
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono: 'Mono Font Name', monospace;
/* Option 2: Use system fonts */
/* --font-sans: ui-sans-serif, system-ui, -apple-system, sans-serif; */
/* Spacing & Layout */
--radius: 0.5rem;
--spacing: 0.25rem;
/* Shadows (optional) */
--shadow-x: 0px;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.17;
--shadow-color: #000000;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
/* Letter spacing (optional) */
--tracking-normal: 0em;
}
/* Dark mode tokens */
[data-theme='your-theme-name'].dark {
/* Same tokens as above, but with dark mode values */
--background: oklch(...);
--foreground: oklch(...);
/* ... all other tokens with dark mode values */
}
/* Theme inline mappings */
[data-theme='your-theme-name'] {
@theme inline {
/* Color mappings */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* Font mappings */
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
/* Radius variants */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Shadow mappings (if shadows are defined) */
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
/* Tracking variants (if tracking-normal is defined) */
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-normal: var(--tracking-normal);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
}
}
```
## Step-by-Step Guide: Adding a New Theme
Follow these steps in order to add a new theme to your application.
### Step 1: Create Theme CSS File
Create a new file in `src/styles/themes/` with a descriptive name (use kebab-case):
```bash
src/styles/themes/your-theme-name.css
```
**Important:** The filename should match the `data-theme` attribute value you'll use in the CSS.
### Step 2: Define Your Theme with `[data-theme]` Attribute
Copy the structure from the "File Format" section above and fill in your color values. Use OKLCH color format for better color consistency:
```css
/* Light mode tokens */
[data-theme='your-theme-name'] {
--background: oklch(1 0 0); /* White */
--foreground: oklch(0.145 0 0); /* Dark gray */
--card: oklch(...);
/* ... all other tokens */
}
/* Dark mode tokens */
[data-theme='your-theme-name'].dark {
--background: oklch(0.145 0 0); /* Dark */
--foreground: oklch(0.985 0 0); /* Light */
/* ... all other tokens with dark mode values */
}
/* Theme inline mappings for Tailwind */
[data-theme='your-theme-name'] {
@theme inline {
/* All the mappings as shown in the File Format section */
}
}
```
**Color Format:**
- Use `oklch()` format: `oklch(lightness chroma hue)`
- Example: `oklch(0.852 0.199 91.936)` = light green-blue
- Lightness: 0-1 (0 = black, 1 = white)
- Chroma: 0+ (0 = grayscale, higher = more saturated)
- Hue: 0-360 (color wheel position)
**Key Points:**
- The `[data-theme='your-theme-name']` selector is what makes your theme work
- The value `'your-theme-name'` must match exactly in all places (CSS file, config, etc.)
- Always include both light and dark mode variants
- Include the `@theme inline` block for Tailwind CSS integration
### Step 3: Import Theme in theme.css
Add your theme import to `src/styles/theme.css`:
```css
@import './themes/your-theme-name.css';
```
This makes your theme available to the application.
### Step 4: Add Theme to theme.config.ts
Add your theme to the `THEMES` array in `src/components/themes/theme.config.ts`:
```typescript
export const THEMES = [
// ... existing themes
{
name: 'Your Theme Name', // Display name in the UI
value: 'your-theme-name' // Must match [data-theme] value exactly
}
];
```
**Important:** The `value` field must match the `data-theme` attribute value from your CSS file exactly.
### Step 5: Add Custom Fonts (If Needed)
**Only do this step if your theme requires a custom Google Font that isn't already loaded.**
If you want to use a Google Font in your theme:
**File:** `src/components/themes/font.config.ts`
1. **Import the font** from `next/font/google`:
```typescript
import { Your_Font_Name } from 'next/font/google';
```
2. **Configure the font** with a CSS variable:
```typescript
const fontYourName = Your_Font_Name({
subsets: ['latin'],
weight: ['400', '500', '700'], // Adjust weights as needed
variable: '--font-your-name' // Optional: custom variable name
});
```
3. **Add it to the `fontVariables` export**:
```typescript
export const fontVariables = cn(
// ... existing fonts
fontYourName.variable
);
```
4. **Use the font in your theme CSS** by its display name (not the CSS variable):
```css
[data-theme='your-theme-name'] {
--font-sans: 'Your Font Name', sans-serif; /* Use the actual font name */
--font-mono: 'Your Mono Font', monospace;
}
```
**Important Notes:**
- Use the font's **display name** in CSS (e.g., `'Geist'`, `'Architects Daughter'`), not the CSS variable
- The font must be imported in `font.config.ts` for it to be loaded by Next.js
- Font variables from `font.config.ts` are automatically applied to the body via `layout.tsx`
- You can use any Google Font available in `next/font/google`
- Check existing fonts in `font.config.ts` before adding new ones - you might be able to reuse them
**Example:** The `notebook` theme uses `Architects Daughter`:
- Imported in `font.config.ts` as `Architects_Daughter`
- Used in `notebook.css` as `'Architects Daughter'` (with quotes and space)
### Step 6: Set as Default Theme (Optional)
If you want your theme to be the default theme that loads when users first visit the application (without needing the theme switcher), update the default theme constant:
**File:** `src/components/themes/theme.config.ts`
```typescript
/**
* Default theme that loads when no user preference is set
* Change this value to set a different default theme
*/
export const DEFAULT_THEME = 'your-theme-name'; // Change from 'vercel' to your theme name
```
**Note:**
- This is the **single source of truth** for the default theme - it's automatically used in both server-side rendering and client-side code
- This will make your theme the default for all new users
- Existing users who have already selected a theme will still see their saved preference (stored in cookies)
- The default theme is applied immediately on page load (no flash of unstyled content)
### Step 7: Test Your Theme
1. Start your development server
2. Open the theme selector in the UI
3. Select your new theme
4. Verify it works in both light and dark modes
5. Test scaled variant by selecting "Your Theme Name (Scaled)"
6. If you set it as default, clear your browser cookies and refresh to see it load automatically
## Quick Reference: File Locations
When adding a new theme, you'll work with these files in this order:
1.`src/styles/themes/your-theme-name.css` - Create theme file with `[data-theme]` attribute
2.`src/styles/theme.css` - Import your theme file
3.`src/components/themes/theme.config.ts` - Add theme to `THEMES` array
4. ⚠️ `src/components/themes/font.config.ts` - Add fonts only if needed
5. ⚠️ `src/components/themes/active-theme.tsx` - Set as default only if desired
## Required Tokens
### Minimum Required
At minimum, your theme should define these tokens:
- `--background`
- `--foreground`
- `--card` & `--card-foreground`
- `--popover` & `--popover-foreground`
- `--primary` & `--primary-foreground`
- `--secondary` & `--secondary-foreground`
- `--muted` & `--muted-foreground`
- `--accent` & `--accent-foreground`
- `--destructive` & `--destructive-foreground`
- `--border`
- `--input`
- `--ring`
- `--radius`
### Optional Tokens
These can be omitted if not needed:
- `--chart-1` through `--chart-5` (defaults to primary colors)
- `--sidebar-*` tokens (defaults to card colors)
- `--font-*` tokens (uses system defaults)
- `--shadow-*` tokens (no shadows if omitted)
- `--tracking-normal` (no letter spacing if omitted)
- `--spacing` (uses default)
## Example: Complete Theme
See `src/styles/themes/claude.css` for a complete example with all tokens defined.
## Example: Minimal Theme
For a minimal theme, you can copy an existing theme and modify only the colors you want to change. The system will fall back to defaults for any missing tokens.
## Color Format Reference
### OKLCH Format
```
oklch(lightness chroma hue)
```
- **Lightness**: 0-1 (0 = black, 1 = white)
- **Chroma**: 0+ (0 = grayscale, 0.2+ = colorful)
- **Hue**: 0-360 degrees
- 0/360 = Red
- 60 = Yellow
- 120 = Green
- 180 = Cyan
- 240 = Blue
- 300 = Magenta
### Examples
```css
/* Pure white */
--background: oklch(1 0 0);
/* Pure black */
--foreground: oklch(0 0 0);
/* Bright blue */
--primary: oklch(0.7 0.2 240);
/* Muted gray */
--muted: oklch(0.5 0 0);
```
## Scaled Variants
All themes automatically support scaled variants. When a user selects "Theme Name (Scaled)", the `.theme-scaled` class is applied, which adjusts spacing and text sizes. No additional CSS is needed in your theme file.
## Best Practices
1. **Use descriptive theme names**: Use kebab-case (e.g., `ocean-blue`, `forest-green`)
2. **Provide both light and dark modes**: Always define both variants
3. **Test accessibility**: Ensure sufficient contrast between foreground and background
4. **Keep tokens consistent**: Use similar lightness/chroma values for related colors
5. **Document special features**: If your theme has unique characteristics (like no shadows or custom fonts), add comments
## Troubleshooting
### Theme Not Appearing
- Check that the file is imported in `src/styles/theme.css`
- Verify the theme name matches in both CSS file and theme-selector.tsx
- Ensure the file is saved and the dev server has reloaded
### Colors Not Applying
- Verify all required tokens are defined
- Check that `@theme inline` block includes all color mappings
- Ensure OKLCH format is correct (no typos)
### Dark Mode Not Working
- Verify `.dark` selector is correct: `[data-theme='name'].dark`
- Check that dark mode tokens are defined
- Ensure `next-themes` is properly configured
## Setting a Default Theme
By default, the application uses the `vercel` theme. To change the default theme that loads for new users:
### Change Default Theme Constant
Edit `src/components/themes/theme.config.ts` and update the `DEFAULT_THEME` constant:
```typescript
/**
* Default theme that loads when no user preference is set
* Change this value to set a different default theme
*/
export const DEFAULT_THEME = 'your-theme-name'; // Change this value
```
**How it works:**
- **Single source of truth**: `DEFAULT_THEME` is defined in `theme.config.ts` and imported everywhere it's needed
- **Server-side**: Applied immediately in the HTML `data-theme` attribute (no flash)
- **Client-side**: Used as fallback when no cookie preference exists
- **User preferences**: Still respects saved user preferences (stored in cookies)
- **Automatic**: No need to update multiple files - change it once and it works everywhere
**Benefits of this approach:**
✅ No code duplication - defined once, used everywhere
✅ Type-safe - TypeScript ensures consistency
✅ Easy to change - update one line in one file
✅ Well-documented - clear comments explain its purpose
✅ Immediate application - no flash of unstyled content
## Using Google Fonts in Themes
> **Note:** This section provides additional details about fonts. For the complete step-by-step process, see **Step 5** in the "Step-by-Step Guide" above.
### When to Add Fonts
You only need to add fonts to `font.config.ts` if:
- Your theme uses a Google Font that isn't already imported
- You want to use a custom font that requires loading
**Tip:** Check `src/components/themes/font.config.ts` first - many fonts may already be available!
### Font Loading Process
1. **Import the font** in `src/components/themes/font.config.ts`:
```typescript
import { Roboto, Roboto_Mono } from 'next/font/google';
```
2. **Configure the font** with a CSS variable:
```typescript
const fontRoboto = Roboto({
subsets: ['latin'],
weight: ['400', '500', '700'],
variable: '--font-roboto'
});
```
3. **Add to fontVariables export**:
```typescript
export const fontVariables = cn(
// ... existing fonts
fontRoboto.variable
);
```
4. **Use in your theme CSS** with the font's display name:
```css
[data-theme='your-theme'] {
--font-sans: 'Roboto', sans-serif; /* Use display name, not CSS variable */
--font-mono: 'Roboto Mono', monospace;
}
```
### Important Notes
- **Font names**: Use the font's display name in CSS (e.g., `'Roboto'`, `'Open Sans'`), not the CSS variable name
- **Font loading**: Fonts must be imported in `font.config.ts` to be loaded by Next.js
- **Automatic application**: Font variables are automatically applied to the body element via `layout.tsx`
- **Available fonts**: Check [Next.js Font Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) for available Google Fonts
### Example: Notebook Theme
The `notebook` theme uses `Architects Daughter`:
**In `font.config.ts`:**
```typescript
import { Architects_Daughter } from 'next/font/google';
const fontArchitectsDaughter = Architects_Daughter({
subsets: ['latin'],
weight: '400',
variable: '--font-architects-daughter'
});
export const fontVariables = cn(
// ... other fonts
fontArchitectsDaughter.variable
);
```
**In `notebook.css`:**
```css
[data-theme='notebook'] {
--font-sans: 'Architects Daughter', sans-serif;
}
```
## Reference Files
- **Complete theme example**: `src/styles/themes/claude.css`
- **Theme aggregator**: `src/styles/theme.css`
- **Theme selector component**: `src/components/themes/theme-selector.tsx`
- **Theme provider**: `src/components/themes/active-theme.tsx`
- **Theme configuration** (includes default theme): `src/components/themes/theme.config.ts`
- **Font configuration**: `src/components/themes/font.config.ts`

View File

@@ -0,0 +1,39 @@
# Training Record Draft Feature Report
## Summary
Implemented the draft workflow for training records based on the existing `training-records` feature pattern.
Key behavior now supported:
- Employees can save a training record as `draft`
- Employees can edit their own records only when status is `draft` or `needs_revision`
- Submitting a record moves it to `pending`
- HRD review can return a record as `needs_revision`
- The pending review queue is hard-scoped to `pending` records only
## Main Changes
- Added `draft` to the `approval_status` enum in Drizzle schema
- Generated migration `drizzle/0014_sweet_nico_minoru.sql`
- Extended training-record API types and UI label maps to include `draft`
- Added `submissionMode` handling for create/update flows
- Reset previous review data when a returned record is re-submitted
- Switched employee edit permission from `pending` to `draft | needs_revision`
- Updated pending-review pages to always query only `pending` records
- Updated summary typing in overview and employee-directory to handle the new status safely
## Decisions
- `draft` is a real persisted status, not a client-only flag
- `needs_revision` is reused as the returned-for-revision state
- Draft records are excluded from the HRD pending-review queue
- Draft hours are not counted into review-progress summaries that previously reflected submitted/reviewed work
## Verification
- `npm run gen`
- `npm run lint`
- `npm run build`
All three completed successfully after the implementation updates.

View File

@@ -0,0 +1,64 @@
# Training Record Draft Submit Workflow Report
## Summary
เริ่มลงมือปรับ workflow ฝั่ง `training-records` โดยรอบนี้เน้นให้ปุ่มหลักในฟอร์มสะท้อนสถานะจริงของรายการมากขึ้น และทำให้ action menu ในตารางใช้ข้อความภาษาไทยที่สอดคล้องกับระบบ
## Files Changed
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-tables/cell-action.tsx`
## Requirement Mapping
- Requirement เรื่องปุ่มในหน้า Edit Draft:
ปุ่ม submit หลักแสดง label ตามสถานะจริง เช่น `บันทึกประวัติการอบรม` สำหรับ draft และ `ส่งตรวจสอบอีกครั้ง` สำหรับรายการที่ถูกตีกลับ
- Requirement เรื่อง workflow ของรายการตีกลับ:
ปุ่ม submit หลักรองรับ label สำหรับการ re-submit โดยไม่เปลี่ยน flow ฝั่ง route handler เดิม
- Requirement เรื่องหน้ารายการ:
action menu เปลี่ยนข้อความเป็นไทยเพื่อให้สอดคล้องกับสถานะและการใช้งานในระบบ
## Implementation Details
- เพิ่ม helper ในฟอร์มเพื่อคำนวณข้อความปุ่มหลักจาก `approval_status`
- เพิ่ม helper สำหรับข้อความขณะกำลัง submit เพื่อแยก draft กับ submit ให้ชัดขึ้น
- ซ่อนปุ่ม submit เดิมในฟอร์มไว้ แล้วเรนเดอร์ปุ่ม submit ใหม่ด้วย label ตาม workflow ปัจจุบัน
- ปรับ action menu ในตารางจากข้อความอังกฤษเป็นข้อความไทย
## Button Behavior
- Create:
ปุ่มหลักใช้ข้อความ `ส่งตรวจสอบ`
- Edit Draft:
ปุ่มหลักใช้ข้อความ `บันทึกประวัติการอบรม`
- Edit Needs Revision:
ปุ่มหลักใช้ข้อความ `ส่งตรวจสอบอีกครั้ง`
- ทุกสถานะที่ยังแก้ไขได้:
ปุ่ม `บันทึกฉบับร่าง` ยังแสดงตามเดิม
## Validation Rules
- รอบนี้ยังคงใช้ validation และ route-handler workflow เดิมของฟีเจอร์
- ยังไม่ได้แยก success/error messaging และ invalid-state messaging ตาม intent แบบเต็มรูปแบบในรอบนี้
## Status Workflow
- ฟอร์มและ action ปัจจุบันยังยึด workflow เดิมของระบบ:
`draft -> pending -> approved / needs_revision`
- รายการ `needs_revision` ยังถูกมองเป็นสถานะที่ employee กลับมาแก้ไขและส่งใหม่ได้
## Testing Result
- ยังไม่ได้รัน `lint` หรือ `build` ในรอบนี้
- ตรวจสอบ diff เชิงโค้ดแล้วว่าปุ่ม submit ใหม่อิง `approval_status` จริง
## Impact Analysis
- ไม่มีการเปลี่ยน database schema
- ไม่มีการเปลี่ยน route handler contract
- ไม่มีการเปลี่ยนสิทธิ์การเข้าถึงฝั่ง server
## Notes / Limitations
- ไฟล์ฟอร์มมีข้อความไทยที่มีปัญหา encoding ในบางช่วง ทำให้ patch เฉพาะจุดทำได้ยากกว่าปกติ
- รอบถัดไปควรเก็บงานต่อในส่วน validation/error/success messaging ให้แยกตาม `draft` กับ `submit` แบบครบ flow

View File

@@ -0,0 +1,36 @@
# Training Type Review
## Root Cause
- The form uses TanStack Form, not React Hook Form.
- `trainingType` in create mode was initialized as `undefined`.
- The field-level blur validator had already been improved, but the submit-level Zod schema still used `z.enum(...)`.
- When submit validation ran with an unset value, Zod could still produce a raw type error path like `expected string, received undefined` instead of the intended Thai message.
## Files Changed
- `src/features/training-records/schemas/training-record.ts`
- `src/features/training-records/components/training-record-form.tsx`
## Fixed Code Summary
- Changed `trainingType` form default from `undefined` to `""` in create mode.
- Kept edit mode bound to the stored UI value.
- Updated the `Select` binding to pass `undefined` to Radix only for display when the current form value is empty.
- Replaced submit-level `z.enum(...)` validation with string-based validation plus whitelist refinement so the message always stays:
- `กรุณาเลือกประเภทการอบรม`
- Kept the current app enum contract intact:
- `INTERNAL_TRAINING`
- `EXTERNAL_TRAINING`
- `ONLINE_COURSE`
- `OJT`
## Regression Checklist
- Create mode: open new training record form and submit without selecting training type.
- Create mode: select each training type option and verify no raw Zod error appears.
- Edit mode: open an existing record and confirm the saved type renders correctly in the select.
- Edit mode: change the type and submit successfully.
- Validation after submit: confirm the Thai message appears instead of `Invalid input` or `expected string, received undefined`.
- Table/listing: verify training type badges still render correctly in Training Records and review pages.
- Server normalization: verify create/update payloads still map correctly to legacy DB values.

View File

@@ -0,0 +1,89 @@
# UAT Generation Report
## Executive Summary
ได้ทำการ audit ระบบจาก implementation ปัจจุบันใน repository และจัดทำชุดเอกสาร UAT สำหรับโมดูล runtime หลักของ TMS โดยไม่แก้ source code และไม่เปลี่ยน business logic เอกสารถูกจัดตามโครงสร้างที่ Business User ใช้งานต่อได้ทันที ทั้งในระดับแผนทดสอบ, scenario, test case, permission matrix, regression checklist และ test data guide
## Modules Reviewed
- Authentication
- Dashboard / Navigation
- Organizers
- Users
- Employee Directory
- Courses
- Training Policy
- Training Matrix
- Training Records
- Pending Review
- Announcements
- Online Lessons
- Reports
- Notifications
- Import Employees
- Master Review
- Permission Templates
- User Permissions
- Permission Audit Logs
- Audit Logs
## Features Covered
- Login / access control
- CRUD ของข้อมูลหลัก
- workflow แบบ draft / submit / review / approve / needs revision
- content visibility ตาม publish state
- report filtering และ export
- notifications read workflow
- employee import และ master review
- permission-template-first authorization
- audit trail
## Workflow Summary
- Training Records: draft -> pending -> approved / needs_revision
- Announcements: draft / published / archived
- Online Lessons: draft / published / archived
- Permission Templates: create / edit / clone / activate / deactivate / soft delete
- User Permissions: assign template / allow override / deny override
- Notifications: unread -> read
- Import Employees: upload -> summary -> error CSV / pending master review
## จำนวน Test Scenario
- 66 scenarios
## จำนวน Test Cases
- 74 test cases
## จำนวน Permission Tests
- 18 permission checks
## จำนวน Validation Tests
- 3 explicit validation / upload negative tests
## จำนวน Regression Tests
- 50 regression checklist items
## Missing Features
- ไม่พบ `Settings` เป็น runtime module ที่พร้อม UAT
- ไม่พบ role runtime แบบ `Staff`, `Manager`, `HRD Manager`
- ไม่พบ generic export/import center แยกจาก feature-specific implementation
## Risks
- `plans/uat-test.md` อ้าง Prisma แต่ implementation ปัจจุบันใช้ Drizzle; ผู้ทดสอบควรยึด behavior ปัจจุบันในระบบ
- บาง feature directories ใน repo เป็น legacy/template area แต่ไม่ได้ถูกนำมาใช้ใน nav runtime ปัจจุบัน
- หาก seed data ไม่ครบ อาจทำให้ workflow reports, matrix, approval และ permission scenarios ทดสอบได้ไม่สมบูรณ์
## Recommendations
- เตรียม seed/UAT data ตาม [test-data-guide.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/test-data-guide.md) ก่อนเริ่ม test
- ใช้ persona เพียง 3 กลุ่มหลักตาม auth model ปัจจุบัน: Super Admin, HRD Admin, Employee
- หากต้องการ UAT ระดับ sign-off จริง ควรเพิ่ม execution sheet สำหรับบันทึก `Actual Result`, `Status`, `Defect ID`, `Tester`, `Test Date`
- หากระบบจะรองรับ role เพิ่มในอนาคต ควรออกรอบ permission matrix ใหม่ให้ตรงกับ implementation ตอนนั้น

View File

@@ -0,0 +1,100 @@
# UAT Seed Data Review
## 1. Records generated
- Organization: ชุดข้อมูล UAT 2569 (`uat-2569`)
- HRD owner account: `uat2569.hrd@training.local`
- Employee password: `UAT12345!`
- Employees: 50
- Courses: 40
- Training Records: 250
- Announcements: 15
- Notifications: 300
- Audit Logs: 500
- Training Policy Year: Thai 2569 (stored as Gregorian 2026)
## 2. Distribution summary
### Employee distribution by department
- IT: 8 คน
- Operations: 9 คน
- HRD: 8 คน
- Sales: 8 คน
- Engineering: 9 คน
- Accounting: 8 คน
### Training record distribution by department
- IT: 35 รายการ
- HRD: 30 รายการ
- Accounting: 35 รายการ
- Sales: 45 รายการ
- Engineering: 55 รายการ
- Operations: 50 รายการ
### Training status distribution
- approved: 165 รายการ
- pending: 49 รายการ
- rejected: 36 รายการ
### Approved K/S/A hours distribution
- K: 431.0 ชั่วโมง
- S: 285.0 ชั่วโมง
- A: 181.0 ชั่วโมง
### Monthly approved trend
- 2026-01: 13 รายการอนุมัติ
- 2026-02: 10 รายการอนุมัติ
- 2026-03: 11 รายการอนุมัติ
- 2026-04: 12 รายการอนุมัติ
- 2026-05: 13 รายการอนุมัติ
- 2026-06: 14 รายการอนุมัติ
- 2026-07: 15 รายการอนุมัติ
- 2026-08: 17 รายการอนุมัติ
- 2026-09: 16 รายการอนุมัติ
- 2026-10: 15 รายการอนุมัติ
- 2026-11: 14 รายการอนุมัติ
- 2026-12: 15 รายการอนุมัติ
## 3. Dashboard validation
The generated dataset was shaped to support meaningful dashboard output:
- Department Ranking:
- multiple departments have approved-hour activity
- Engineering and Operations contain higher approved-hour volume
- Course Ranking:
- repeated course usage creates stable top-course results
- expected top courses:
- Project Management Fundamentals: 92.0 ชั่วโมง
- การบริหารคลังสินค้าและรหัสสินค้า: 68.0 ชั่วโมง
- Secure Coding Awareness: 49.0 ชั่วโมง
- Root Cause Analysis: 49.0 ชั่วโมง
- เทคนิคการเจรจาต่อรองการขาย: 42.0 ชั่วโมง
- K/S/A Distribution:
- approved records include mixed K/S/A category coverage
- Monthly Trend:
- approved records are spread across all 12 months of Thai year 2569
## 4. Report validation
The seed set supports report verification for:
- employee-level training history
- HRD organization-wide pending review visibility
- approved vs pending vs rejected comparison
- yearly filtering for Thai year 2569
- course-level and department-level aggregation
- notification center pagination and unread counts
- audit log filtering by module and action
## 5. Known limitations
- Thai year 2569 is stored in the database as Gregorian year 2026 because application queries use Gregorian year boundaries
- Seeded notifications and announcements include the [UAT2569] marker for easy identification during UAT
- No certificate files are generated in this seed set
- This script seeds a dedicated UAT organization and does not try to merge into existing business organizations

78
docs/uat-test-cases.md Normal file
View File

@@ -0,0 +1,78 @@
# UAT Test Cases
| Test ID | Module | Feature | Role | Priority | Preconditions | Test Steps | Expected Result | Actual Result | Status |
| -------- | --------------------- | ------------------------------ | -------------------- | -------- | -------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------- | ------- |
| AUTH-001 | Authentication | Sign In | Super Admin | High | มีบัญชี super admin | 1. เปิด `/auth/sign-in` 2. กรอก credentials 3. กดเข้าสู่ระบบ | เข้าสู่ระบบสำเร็จและเห็นเมนูระดับ super admin | | Not Run |
| AUTH-002 | Authentication | Sign In | HRD Admin | High | มีบัญชี HRD Admin | 1. เปิดหน้า sign in 2. login | เข้าสู่ dashboard ได้สำเร็จ | | Not Run |
| AUTH-003 | Authentication | Sign In | Employee | High | มีบัญชี employee | 1. เปิดหน้า sign in 2. login | เข้าสู่ dashboard ได้สำเร็จ | | Not Run |
| AUTH-004 | Authentication | Protected Route | Guest | High | logout แล้ว | 1. เปิด `/dashboard/overview` | ถูก redirect ไป sign-in หรือถูกปฏิเสธการเข้าถึง | | Not Run |
| NAV-001 | Navigation | Sidebar Visibility | Super Admin | High | login เป็น super admin | 1. เปิด sidebar | เห็นเมนู permissions, organizers, audit logs | | Not Run |
| NAV-002 | Navigation | Sidebar Visibility | HRD Admin | High | login เป็น HRD Admin | 1. เปิด sidebar | เห็นเฉพาะเมนู HRD ที่อยู่ใน nav config | | Not Run |
| NAV-003 | Navigation | Sidebar Visibility | Employee | High | login เป็น employee | 1. เปิด sidebar | ไม่เห็นเมนูจัดการระบบ เช่น users, permissions, organizers | | Not Run |
| ORG-001 | Organizers | List | Super Admin | High | login เป็น super admin | 1. เปิด `/dashboard/organizers` | เห็นรายการ organizers | | Not Run |
| ORG-002 | Organizers | Create | Super Admin | High | login เป็น super admin | 1. เปิดหน้าจัดการ organizer 2. กรอกข้อมูล 3. บันทึก | สร้าง organizer สำเร็จ | | Not Run |
| ORG-003 | Organizers | Unauthorized Access | HRD Admin | High | login เป็น HRD Admin | 1. เปิด `/dashboard/organizers` | ไม่สามารถเข้าถึงได้ | | Not Run |
| USER-001 | Users | List and Search | HRD Admin | High | login เป็น HRD Admin | 1. เปิด `/dashboard/users` 2. ค้นหาชื่อหรือ email | ตารางแสดงผลตรงกับ keyword | | Not Run |
| USER-002 | Users | Filter and Pagination | HRD Admin | Medium | มี user หลายรายการ | 1. filter status 2. เปลี่ยนหน้า | filter และ pagination ทำงานถูกต้อง | | Not Run |
| USER-003 | Users | Create User | HRD Admin | High | มี department/position | 1. กดเพิ่ม user 2. กรอกข้อมูล 3. บันทึก | สร้าง user และ membership ใน organization ปัจจุบันสำเร็จ | | Not Run |
| USER-004 | Users | Edit User | HRD Admin | High | มี user เป้าหมาย | 1. เปิดแก้ไข user 2. ปรับข้อมูล 3. บันทึก | ข้อมูลถูกอัปเดตสำเร็จ | | Not Run |
| USER-005 | Users | Scope Restriction | Employee | High | login เป็น employee | 1. เปิด `/dashboard/users` | เข้าถึงไม่ได้ | | Not Run |
| EMP-001 | Employee Directory | List | HRD Admin | High | login เป็น HRD Admin | 1. เปิด `/dashboard/employee-directory` | เห็นรายการพนักงานทั้งหมดใน organization | | Not Run |
| EMP-002 | Employee Directory | Detail | HRD Admin | Medium | มีพนักงานในระบบ | 1. เปิดรายละเอียดพนักงาน | เห็นข้อมูลประวัติพนักงานและ training-related data | | Not Run |
| EMP-003 | Employee Directory | Individual Target Update | HRD Admin | Medium | มี employee target route | 1. เปิด target ของพนักงาน 2. ปรับค่า 3. บันทึก | target ถูกบันทึกสำเร็จ | | Not Run |
| CRS-001 | Courses | List / Search | HRD Admin | High | login เป็น HRD Admin | 1. เปิด `/dashboard/courses` 2. ค้นหา | แสดงรายการตามเงื่อนไข | | Not Run |
| CRS-002 | Courses | Create | HRD Admin | High | - | 1. กดเพิ่ม course 2. กรอกข้อมูล 3. บันทึก | สร้าง course สำเร็จ | | Not Run |
| CRS-003 | Courses | Edit | HRD Admin | High | มี course เดิม | 1. เปิดแก้ไข 2. ปรับค่า 3. บันทึก | อัปเดต course สำเร็จ | | Not Run |
| POL-001 | Training Policy | Create / Update | HRD Admin | Medium | login เป็น HRD Admin | 1. เปิด `/dashboard/training-policy` 2. กรอก target 3. บันทึก | policy ถูกบันทึกสำเร็จ | | Not Run |
| TMX-001 | Training Matrix | Create Rule by Department | HRD Admin | High | มี department และ course | 1. เปิด `/dashboard/training-matrix` 2. สร้าง rule | rule ถูกสร้างสำเร็จ | | Not Run |
| TMX-002 | Training Matrix | Create Rule by Position | HRD Admin | High | มี position และ course | 1. สร้าง rule แบบ position -> course | rule ถูกสร้างสำเร็จ | | Not Run |
| TMX-003 | Training Matrix | Compliance View | HRD Admin | Medium | มีข้อมูล approved record | 1. เปิด compliance view | compliance แสดงผลตามข้อมูลจริง | | Not Run |
| TR-001 | Training Records | Create for Employee | HRD Admin | High | มี employee และ course | 1. เปิด `/dashboard/training-records` 2. กดสร้าง 3. เลือกพนักงานและหลักสูตร 4. บันทึก | record ถูกสร้างสำเร็จ | | Not Run |
| TR-002 | Training Records | Create Self Record | Employee | High | login เป็น employee | 1. เปิด training records 2. สร้างรายการใหม่ | สร้างได้โดยผูกกับข้อมูลของตนเองเท่านั้น | | Not Run |
| TR-003 | Training Records | Save Draft | Employee | High | กรอกฟอร์มขั้นต่ำแล้ว | 1. กดบันทึกฉบับร่าง | status เป็น `draft` | | Not Run |
| TR-004 | Training Records | Submit | Employee | High | มี draft หรือฟอร์มครบ | 1. กด submit | status เป็น `pending` | | Not Run |
| TR-005 | Training Records | Edit Draft | Employee | High | มี record สถานะ `draft` | 1. เปิดแก้ไข draft 2. ปรับข้อมูล 3. บันทึก | แก้ไขได้สำเร็จ | | Not Run |
| TR-006 | Training Records | Attachment Upload | Employee | High | มี record ที่แก้ไขได้ | 1. แนบไฟล์ที่รองรับ 2. บันทึก | upload สำเร็จและเห็น attachment ในรายการ | | Not Run |
| TR-007 | Training Records | List Filter | HRD Admin | Medium | มีหลาย record | 1. filter status / type / year / company / department | ตารางแสดงผลถูกต้อง | | Not Run |
| TR-008 | Training Records | Self Scope | Employee | High | มี record ของหลายคน | 1. login เป็น employee 2. เปิด list | เห็นเฉพาะรายการของตัวเอง | | Not Run |
| TR-009 | Pending Review | Approve | HRD Admin | High | มี record สถานะ `pending` | 1. เปิด pending review 2. กรอก approved minutes/category 3. อนุมัติ | status เป็น `approved` และ reviewer note ถูกบันทึก | | Not Run |
| TR-010 | Pending Review | Needs Revision | HRD Admin | High | มี record สถานะ `pending` | 1. เปิด pending review 2. ใส่เหตุผล 3. ตีกลับ | status เป็น `needs_revision` | | Not Run |
| TR-011 | Pending Review | Unauthorized Review | Employee | High | login เป็น employee | 1. พยายามเปิด review page หรือ review API | ถูกปฏิเสธการเข้าถึง | | Not Run |
| TR-012 | Training Records | Approved Record Locked | Employee | High | มี approved record ของตัวเอง | 1. เปิดหน้ารายการ approved 2. พยายามแก้ไข | ไม่สามารถแก้ไขได้ตาม policy | | Not Run |
| ANN-001 | Announcements | Create | HRD Admin | High | login เป็น HRD Admin | 1. เปิด `/dashboard/announcements` 2. สร้าง announcement | บันทึก announcement สำเร็จ | | Not Run |
| ANN-002 | Announcements | Update / Pin | HRD Admin | Medium | มี announcement เดิม | 1. เปิดแก้ไข 2. ปรับข้อความหรือ pin 3. บันทึก | ข้อมูลถูกอัปเดตสำเร็จ | | Not Run |
| ANN-003 | Announcements | Visibility by Published Window | Employee | High | มีประกาศ published และหมดอายุบางรายการ | 1. login เป็น employee 2. เปิด announcements | เห็นเฉพาะ published ที่ active ตามช่วงเวลา | | Not Run |
| ANN-004 | Announcements | Attachment Download | Employee | Medium | มี announcement ที่แนบไฟล์ | 1. เปิด announcement 2. ดาวน์โหลด attachment | เปิดหรือดาวน์โหลดไฟล์ได้ | | Not Run |
| OLS-001 | Online Lessons | Create | HRD Admin | High | login เป็น HRD Admin | 1. เปิดหน้าจัดการ lesson 2. กรอกข้อมูล 3. บันทึก | สร้าง lesson สำเร็จ | | Not Run |
| OLS-002 | Online Lessons | Update with Video / Attachment | HRD Admin | High | มี lesson เดิม | 1. แก้ไข 2. เปลี่ยน video url หรืออัปโหลดไฟล์ 3. บันทึก | lesson ถูกอัปเดตสำเร็จ | | Not Run |
| OLS-003 | Online Lessons | Published Visibility | Employee | High | มี lesson published และ draft | 1. login เป็น employee 2. เปิดหน้า lessons | เห็นเฉพาะ `is_published = true` | | Not Run |
| OLS-004 | Online Lessons | Search / Category Filter | All | Medium | มี lesson หลาย category | 1. ใช้ search 2. filter category | ผลลัพธ์ตรงเงื่อนไข | | Not Run |
| RPT-001 | Reports | Organization Report Access | HRD Admin | High | login เป็น HRD Admin | 1. เปิด `/dashboard/reports` 2. เปลี่ยน year/company/department | เห็น report หลายชุดระดับ organization | | Not Run |
| RPT-002 | Reports | Self Report Access | Employee | High | login เป็น employee | 1. เปิด reports | เห็นเฉพาะ employee transcript ของตัวเอง | | Not Run |
| RPT-003 | Reports | Excel Export | HRD Admin | High | มี report data | 1. กด export Excel | ดาวน์โหลด `.xlsx` สำเร็จ | | Not Run |
| RPT-004 | Reports | PDF Export | HRD Admin | High | มี report data | 1. กด export PDF | ดาวน์โหลด `.pdf` สำเร็จ | | Not Run |
| RPT-005 | Reports | Self Scope via Query String | Employee | High | login เป็น employee | 1. แก้ URL filter เอง 2. refresh | server ยังบังคับให้เห็นเฉพาะข้อมูลตนเอง | | Not Run |
| NTF-001 | Notifications | List / Filter | All | Medium | มี notifications ในระบบ | 1. เปิด `/dashboard/notifications` 2. filter status/type | แสดงรายการตามตัวกรอง | | Not Run |
| NTF-002 | Notifications | Mark One as Read | All | Medium | มี unread notification | 1. กด mark as read รายการเดียว | สถานะรายการเปลี่ยนเป็น read | | Not Run |
| NTF-003 | Notifications | Mark All as Read | All | Medium | มี unread notification หลายรายการ | 1. กดอ่านทั้งหมด | unread count ลดลงเป็น 0 หรือเท่าที่เหลือจริง | | Not Run |
| IMP-001 | Import Employees | Download Template | HRD Admin | Medium | login เป็น HRD Admin | 1. เปิด import page 2. ดาวน์โหลด template | ได้ไฟล์ template สำหรับ import | | Not Run |
| IMP-002 | Import Employees | Import Success | HRD Admin | High | มีไฟล์ `.xlsx` ถูกต้อง | 1. เลือกไฟล์ 2. import | ระบบคืน summary ของ created/updated/linked/training record | | Not Run |
| IMP-003 | Import Employees | Error Summary Download | HRD Admin | Medium | มีไฟล์ที่ทำให้บางแถวผิด | 1. import 2. ดาวน์โหลด error summary | ได้ไฟล์ CSV ของรายการ error | | Not Run |
| MREV-001 | Master Review | Review Pending Items | HRD Admin | Medium | มี pending master review | 1. เปิดหน้าตรวจสอบข้อมูลหลัก | เห็นรายการ company/department/position ที่ pending | | Not Run |
| MREV-002 | Master Review | Approve / Reject | HRD Admin | High | มี pending item | 1. กด approve หรือ reject | สถานะถูกประมวลผลและมี notification ที่เกี่ยวข้อง | | Not Run |
| PERM-001 | Permission Templates | Create Template | Super Admin | High | login เป็น super admin | 1. เปิด `/dashboard/permissions/templates` 2. สร้าง template ใหม่ | สร้าง template สำเร็จ | | Not Run |
| PERM-002 | Permission Templates | Edit Template | Super Admin | High | มี template เดิม | 1. เปิด edit 2. ปรับ permissions 3. บันทึก | template ถูกอัปเดตสำเร็จ | | Not Run |
| PERM-003 | Permission Templates | Clone Template | Super Admin | Medium | มี template เดิม | 1. เลือก clone | ได้ template ใหม่จากต้นฉบับ | | Not Run |
| PERM-004 | Permission Templates | Activate / Deactivate | Super Admin | Medium | มี template ที่ active/inactive | 1. toggle สถานะ | สถานะเปลี่ยนถูกต้อง | | Not Run |
| PERM-005 | Permission Templates | Soft Delete | Super Admin | Medium | มี non-system template | 1. ลบ template | template ถูก soft delete / ปิดการใช้งานตาม flow | | Not Run |
| PERM-006 | User Permissions | Assign Template to User | Super Admin | High | มี user และ template | 1. เปิด `/dashboard/permissions/users` 2. manage permissions 3. เลือก template 4. save | assignment ถูกบันทึกสำเร็จ | | Not Run |
| PERM-007 | User Permissions | Override Allow / Deny | Super Admin | High | มี user permission detail | 1. assign allow/deny overrides 2. save | effective permissions เปลี่ยนตามที่เลือก | | Not Run |
| PERM-008 | Permission Matrix | View Catalog | Super Admin | Medium | login เป็น super admin | 1. เปิด `/dashboard/permissions/matrix` | เห็น permission catalog และกลุ่มสิทธิ์ | | Not Run |
| PERM-009 | Permission Audit Logs | View Audit | Super Admin | Medium | มีการแก้ permission มาก่อน | 1. เปิด `/dashboard/permissions/audit-logs` | เห็นประวัติการเปลี่ยน permission | | Not Run |
| AUD-001 | Audit Logs | View System Audit Logs | Super Admin | Medium | มี audit logs ในระบบ | 1. เปิด `/dashboard/audit-logs` | เห็นรายการ audit log | | Not Run |
| AUD-002 | Audit Logs | Unauthorized Access | HRD Admin | High | login เป็น HRD Admin | 1. เปิด `/dashboard/audit-logs` | เข้าถึงไม่ได้ | | Not Run |
| VAL-001 | Validation | Required Field | All | High | เปิดฟอร์มสร้างรายการ | 1. submit โดยไม่กรอก field บังคับ | แสดง validation message ที่เข้าใจได้ | | Not Run |
| VAL-002 | Upload | Invalid File Type | All | High | อยู่ในฟอร์ม upload | 1. แนบไฟล์ประเภทไม่รองรับ | ระบบปฏิเสธไฟล์และแจ้งข้อความผิดพลาด | | Not Run |
| VAL-003 | Upload | Oversize File | All | High | มีไฟล์ขนาดเกินข้อกำหนด | 1. แนบไฟล์ขนาดใหญ่เกิน limit | ระบบปฏิเสธไฟล์และแจ้งข้อความผิดพลาด | | Not Run |
| SEC-001 | Organization Scope | Cross-Organization Access | All | High | มีข้อมูลข้าม organization | 1. พยายามเปิดข้อมูลของ org อื่นผ่าน URL/API | ไม่สามารถเข้าถึงได้ | | Not Run |
| SEC-002 | Permission Management | Non-Super-Admin Access | HRD Admin / Employee | High | login เป็น non-super-admin | 1. เปิดหน้ากลุ่ม permissions | เข้าถึงไม่ได้ | | Not Run |

115
docs/uat-test-plan.md Normal file
View File

@@ -0,0 +1,115 @@
# UAT Test Plan
## 1. วัตถุประสงค์
เอกสารนี้ใช้สำหรับวางแผน User Acceptance Testing (UAT) ของ Training Management System (TMS) โดยอ้างอิงจาก implementation ปัจจุบันใน repository เพื่อให้ Business User สามารถนำไปใช้ทดสอบจริงได้ทันที
## 2. ขอบเขตที่ตรวจจากโค้ดจริง
โมดูล runtime ที่พบและอยู่ในขอบเขต UAT:
- Authentication
- Dashboard / Overview
- Training Records
- Pending Review
- Courses
- Training Policy
- Training Matrix
- Users
- Employee Directory
- Announcements
- Online Lessons
- Reports
- Notifications
- Import Employees
- Master Review
- Permission Templates
- User Permissions
- Permission Audit Logs
- Audit Logs
- Organizers
## 3. นอกขอบเขตหรือยังไม่พบเป็น runtime module ชัดเจน
- `Settings` ยังไม่พบเป็นโมดูล dashboard ที่พร้อมใช้งานจริง
- role แบบ `Staff`, `Manager`, `HRD Manager` ไม่พบเป็น role runtime ใน auth model ปัจจุบัน
- generic export center ไม่พบแยกเป็นโมดูลกลาง
- generic attachment center ไม่พบแยกเป็นโมดูลกลาง
## 4. Architecture Baseline ที่ใช้ในการทดสอบ
อ้างอิงจากโค้ดจริง:
- Frontend: Next.js 16 App Router + TypeScript
- Persistence: Drizzle ORM + PostgreSQL
- Auth: Auth.js Credentials
- Forms: TanStack Form + Zod
- Data fetching: TanStack Query
- UI: shadcn/ui
- Permission Model: Permission Template + server-side RBAC
- Workflow ที่พบจริง: draft, pending, approved, rejected, needs_revision, published, archived
หมายเหตุ:
- แผน `plans/uat-test.md` อ้างถึง Prisma แต่ implementation ปัจจุบันใช้ Drizzle ORM
## 5. Persona สำหรับ UAT
ใช้ persona ตามระบบปัจจุบัน:
| Persona | อิงจากโค้ด | ใช้ทดสอบ |
| ----------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| Super Admin | `systemRole = super_admin` | organizer management, permission management, audit logs |
| HRD Admin | membership `owner/admin` -> `businessRole = HRD` | users, employee directory, courses, training, reports, announcements, online lessons |
| Employee | membership `member` -> `businessRole = EMPLOYEE` | own training records, public announcements, public online lessons, own reports, notifications |
## 6. Test Environment / Entry Criteria
- ตั้งค่า `.env.local` และเชื่อมต่อฐานข้อมูลได้
- migrate schema เรียบร้อย
- dev server รันได้
- มี test organization อย่างน้อย 1 รายการ
- มี department / position / course ขั้นต่ำสำหรับ flow หลัก
- มีบัญชีทดสอบ 3 persona ตามตารางข้างต้น
## 7. Exit Criteria
- Happy path ของทุกโมดูลหลักผ่าน
- Permission test ตาม persona ผ่าน
- Validation / negative case สำคัญผ่าน
- Regression checklist ผ่านหลังแก้ defect
- defect ที่เหลือถูกบันทึกพร้อม severity และ owner
## 8. ลำดับแนะนำในการทดสอบ
1. Authentication และ route protection
2. Organizer / organization context
3. Users และ Employee Directory
4. Courses / Training Policy / Training Matrix
5. Training Records และ Approval
6. Announcements / Online Lessons
7. Reports / Export
8. Notifications
9. Import Employees / Master Review
10. Permission Management / Audit Logs
## 9. ประเภทการทดสอบที่ต้องครอบคลุม
- Happy Path
- CRUD
- Validation
- Permission / Scope isolation
- Workflow / Approval
- Search / Filter / Pagination
- Upload / Download / Export
- Negative Case
- Regression
## 10. เอกสารที่เกี่ยวข้อง
- [uat-test-scenarios.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/uat-test-scenarios.md)
- [uat-test-cases.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/uat-test-cases.md)
- [permission-test-matrix.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/permission-test-matrix.md)
- [regression-test-checklist.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/regression-test-checklist.md)
- [test-data-guide.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/test-data-guide.md)
- [uat-generation-report.md](/D:/WY-2569/HRD/training-system-minimal-refactor/docs/uat-generation-report.md)

137
docs/uat-test-scenarios.md Normal file
View File

@@ -0,0 +1,137 @@
# UAT Test Scenarios
## Authentication
| Scenario ID | Module | Scenario | Persona |
| ----------- | -------------- | ------------------------------------------------ | ----------- |
| AUTH-TS-001 | Authentication | Login ด้วยบัญชี Super Admin สำเร็จ | Super Admin |
| AUTH-TS-002 | Authentication | Login ด้วยบัญชี HRD Admin สำเร็จ | HRD Admin |
| AUTH-TS-003 | Authentication | Login ด้วยบัญชี Employee สำเร็จ | Employee |
| AUTH-TS-004 | Authentication | เปิด protected route โดยไม่ login ต้องถูกป้องกัน | All |
## Dashboard / Navigation
| Scenario ID | Module | Scenario | Persona |
| ----------- | ---------- | -------------------------------- | ------- |
| DASH-TS-001 | Dashboard | Dashboard แสดงข้อมูลตาม persona | All |
| DASH-TS-002 | Navigation | Sidebar แสดงเมนูตาม role ถูกต้อง | All |
| DASH-TS-003 | Navigation | ผู้ใช้ไม่เห็นเมนูที่ไม่มีสิทธิ์ | All |
## Organizers
| Scenario ID | Module | Scenario | Persona |
| ----------- | ---------- | ----------------------------------------- | ----------- |
| ORG-TS-001 | Organizers | ดูรายการ organizers ได้ | Super Admin |
| ORG-TS-002 | Organizers | สร้าง organizer ใหม่ได้ | Super Admin |
| ORG-TS-003 | Organizers | ผู้ใช้ที่ไม่ใช่ Super Admin เข้าถึงไม่ได้ | HRD Admin |
## Users / Employee Directory
| Scenario ID | Module | Scenario | Persona |
| ----------- | ------------------ | -------------------------------------------------------------- | --------- |
| USER-TS-001 | Users | เปิดรายการผู้ใช้และใช้ search / filter / sort / pagination ได้ | HRD Admin |
| USER-TS-002 | Users | สร้างผู้ใช้ใหม่ใน organization ปัจจุบันได้ | HRD Admin |
| USER-TS-003 | Users | แก้ไขข้อมูลผู้ใช้ได้ | HRD Admin |
| EMP-TS-001 | Employee Directory | ดู directory ได้และ filter ได้ | HRD Admin |
| EMP-TS-002 | Employee Directory | เปิดรายละเอียดพนักงานได้ | HRD Admin |
| EMP-TS-003 | Employee Directory | ปรับ training target รายบุคคลได้ | HRD Admin |
| EMP-TS-004 | Employee Directory | Employee เข้าถึง employee directory ไม่ได้ | Employee |
## Courses / Training Policy / Training Matrix
| Scenario ID | Module | Scenario | Persona |
| ----------- | --------------- | ----------------------------------------------- | --------- |
| CRS-TS-001 | Courses | ดูรายการหลักสูตรและค้นหาได้ | HRD Admin |
| CRS-TS-002 | Courses | สร้างหลักสูตรใหม่ได้ | HRD Admin |
| CRS-TS-003 | Courses | แก้ไขหลักสูตรได้ | HRD Admin |
| POL-TS-001 | Training Policy | สร้างหรือแก้ไข training policy ได้ | HRD Admin |
| TMX-TS-001 | Training Matrix | สร้าง matrix rule ตาม department / position ได้ | HRD Admin |
| TMX-TS-002 | Training Matrix | ดู compliance data ได้ | HRD Admin |
## Training Records / Approval
| Scenario ID | Module | Scenario | Persona |
| ----------- | ---------------- | --------------------------------------------- | -------------------- |
| TR-TS-001 | Training Records | HRD Admin สร้าง training record ให้พนักงานได้ | HRD Admin |
| TR-TS-002 | Training Records | Employee สร้าง training record ของตนเองได้ | Employee |
| TR-TS-003 | Training Records | Save Draft ได้ | HRD Admin / Employee |
| TR-TS-004 | Training Records | Submit training record ได้ | HRD Admin / Employee |
| TR-TS-005 | Training Records | แนบ certificate ที่รองรับได้ | HRD Admin / Employee |
| TR-TS-006 | Pending Review | Reviewer อนุมัติรายการได้ | HRD Admin |
| TR-TS-007 | Pending Review | Reviewer ตีกลับรายการเพื่อแก้ไขได้ | HRD Admin |
| TR-TS-008 | Training Records | Employee เห็นเฉพาะรายการของตนเอง | Employee |
| TR-TS-009 | Training Records | Employee แก้ approved record ไม่ได้ | Employee |
## Announcements
| Scenario ID | Module | Scenario | Persona |
| ----------- | ------------- | --------------------------------------------------------------- | -------------------- |
| ANN-TS-001 | Announcements | HRD Admin สร้าง announcement ได้ | HRD Admin |
| ANN-TS-002 | Announcements | HRD Admin แก้ไข announcement ได้ | HRD Admin |
| ANN-TS-003 | Announcements | Employee เห็นเฉพาะ announcement ที่ published และอยู่ในช่วงเวลา | Employee |
| ANN-TS-004 | Announcements | แนบ attachment และเปิดดูได้ | HRD Admin / Employee |
## Online Lessons
| Scenario ID | Module | Scenario | Persona |
| ----------- | -------------- | --------------------------------------- | --------- |
| OLS-TS-001 | Online Lessons | HRD Admin สร้าง online lesson ได้ | HRD Admin |
| OLS-TS-002 | Online Lessons | HRD Admin แก้ไข lesson และไฟล์ประกอบได้ | HRD Admin |
| OLS-TS-003 | Online Lessons | Employee เห็นเฉพาะ lesson ที่ published | Employee |
| OLS-TS-004 | Online Lessons | Filter ตาม category และ search ได้ | All |
## Reports / Export
| Scenario ID | Module | Scenario | Persona |
| ----------- | ------- | ---------------------------------------------- | -------------------- |
| RPT-TS-001 | Reports | HRD Admin ดู report แบบ organization scope ได้ | HRD Admin |
| RPT-TS-002 | Reports | Employee ดูได้เฉพาะ transcript ของตนเอง | Employee |
| RPT-TS-003 | Reports | Export Excel ได้ | HRD Admin / Employee |
| RPT-TS-004 | Reports | Export PDF ได้ | HRD Admin / Employee |
## Notifications
| Scenario ID | Module | Scenario | Persona |
| ----------- | ------------- | -------------------------------------- | ------- |
| NTF-TS-001 | Notifications | เปิดรายการ notification และ filter ได้ | All |
| NTF-TS-002 | Notifications | mark as read รายการเดียวได้ | All |
| NTF-TS-003 | Notifications | mark all as read ได้ | All |
## Import Employees / Master Review
| Scenario ID | Module | Scenario | Persona |
| ----------- | ---------------- | -------------------------------------------------- | --------- |
| IMP-TS-001 | Import Employees | ดาวน์โหลด template import ได้ | HRD Admin |
| IMP-TS-002 | Import Employees | import ไฟล์ `.xlsx` แล้วได้ summary/result ถูกต้อง | HRD Admin |
| IMP-TS-003 | Import Employees | ดาวน์โหลด error summary ได้เมื่อ import มี error | HRD Admin |
| MREV-TS-001 | Master Review | ดูรายการ master data pending review ได้ | HRD Admin |
| MREV-TS-002 | Master Review | approve / reject master review item ได้ | HRD Admin |
## Permission Management
| Scenario ID | Module | Scenario | Persona |
| ----------- | --------------------- | ---------------------------------------------------- | ----------- |
| PERM-TS-001 | Permission Templates | ดูรายการ templates ได้ | Super Admin |
| PERM-TS-002 | Permission Templates | สร้าง template ได้ | Super Admin |
| PERM-TS-003 | Permission Templates | แก้ไข / clone template ได้ | Super Admin |
| PERM-TS-004 | Permission Templates | activate / deactivate / soft delete template ได้ | Super Admin |
| PERM-TS-005 | User Permissions | assign template และ override permission ให้ user ได้ | Super Admin |
| PERM-TS-006 | Permission Matrix | ดู permission catalog / matrix ได้ | Super Admin |
| PERM-TS-007 | Permission Audit Logs | ดู audit logs ของ permission actions ได้ | Super Admin |
## Audit Logs
| Scenario ID | Module | Scenario | Persona |
| ----------- | ---------- | ----------------------------- | -------------------- |
| AUD-TS-001 | Audit Logs | Super Admin ดู audit logs ได้ | Super Admin |
| AUD-TS-002 | Audit Logs | ผู้ใช้ทั่วไปเข้าถึงไม่ได้ | HRD Admin / Employee |
## Negative / Validation / Scope
| Scenario ID | Module | Scenario | Persona |
| ----------- | ----------- | ------------------------------------------------------------ | -------------------- |
| NEG-TS-001 | Validation | required field ต้องแสดงข้อความผิดพลาดที่เข้าใจได้ | All |
| NEG-TS-002 | Upload | ไฟล์ผิดประเภทหรือเกินขนาดต้องถูกปฏิเสธ | All |
| NEG-TS-003 | RBAC | ผู้ใช้ข้าม organization scope ไม่ได้ | All |
| NEG-TS-004 | Reports | Employee ปรับ query string แล้วต้องยังเห็นเฉพาะข้อมูลตนเอง | Employee |
| NEG-TS-005 | Permissions | ผู้ใช้ไม่มีสิทธิ์ไม่สามารถเปิดหน้า permission management ได้ | HRD Admin / Employee |

View File

@@ -0,0 +1,337 @@
# UI/UX Audit Sprint 6.12
## Executive Summary
- Audit type: code-and-structure review of current UI implementation before Sprint 7
- Audit basis: current page composition, shared layout/components, responsive class behavior, table/form/dialog patterns
- Overall UI score: 7.2/10
- Overall UX score: 6.9/10
- Overall Responsive score: 6.8/10
The application already has a solid shared shell, reusable table layer, and consistent shadcn-based primitives. The main gaps are inconsistency between older page-specific patterns and newer shared components, mixed language/tone across screens, crowded dashboards, and form/table experiences that are functionally correct but still feel dense on mobile and mid-width screens.
## Files Reviewed
- `src/components/layout/page-container.tsx`
- `src/components/layout/app-sidebar.tsx`
- `src/components/layout/header.tsx`
- `src/components/ui/table.tsx`
- `src/components/ui/table/data-table.tsx`
- `src/components/ui/table/data-table-toolbar.tsx`
- `src/components/ui/table/data-table-pagination.tsx`
- `src/components/ui/dialog.tsx`
- `src/components/ui/drawer.tsx`
- `src/components/ui/tanstack-form.tsx`
- `src/features/overview/components/overview-filter-panel.tsx`
- `src/app/dashboard/overview/layout.tsx`
- `src/features/training-records/components/training-record-tables/index.tsx`
- `src/features/training-records/components/training-record-tables/columns.tsx`
- `src/features/training-records/components/training-record-form.tsx`
- `src/features/training-records/components/training-record-view-page.tsx`
- `src/features/training-records/components/pending-review-table.tsx`
- `src/features/employee-directory/components/employee-directory-table.tsx`
- `src/features/employee-directory/components/employee-directory-toolbar.tsx`
- `src/features/employee-directory/components/employee-directory-detail-page.tsx`
- `src/features/users/components/users-table/index.tsx`
- `src/features/users/components/user-form-sheet.tsx`
- `src/features/reports/components/reports-page-content.tsx`
- `src/features/reports/components/report-table-card.tsx`
## Components Reviewed
- Sidebar
- Header
- Page container
- DataTable
- DataTable toolbar
- DataTable pagination
- Dialog
- Drawer
- Card patterns
- Form patterns
- Sheet form pattern
- Report table cards
## Screens Reviewed
- Employee Dashboard
- HRD Dashboard
- Training Records
- Create Training Record
- Edit Training Record
- Pending Review
- Employee Directory
- Employee Detail
- Users
- Reports
## Page-by-Page Review
### Dashboard
#### Strengths
- Shared `PageContainer` gives pages a predictable shell.
- KPI cards are already separated from filter controls and chart sections.
- Role-based dashboard branching is clear and prevents irrelevant content.
#### Problems
- Dashboard filter panels are built with native `select` controls and page-specific styling instead of a shared filter component.
- KPI cards mix useful metrics with low-information badges; some badges render with little visual meaning.
- The admin dashboard grid is content-heavy and risks weak hierarchy on tablet widths.
- Chart and card density is high before enough whitespace separates sections.
#### Suggested Improvements
- Standardize dashboard filters into one reusable filter-bar component shared by Overview and Reports.
- Reduce secondary badge noise on KPI cards and surface only one key supporting datum.
- Increase sectional separation between cards, charts, and recent activity panels.
- Introduce a consistent empty-state design for dashboard subpanels.
#### Priority
- Medium
### Training Records
#### Strengths
- Reusable DataTable pattern is in place.
- Status and training-type badges improve scannability.
- Certificate preview has a dedicated component and modal flow.
#### Problems
- Table still depends on many medium-width columns, so discoverability on mobile remains limited even when overflow is contained.
- Certificate column is useful but visually expensive relative to the information density of the row.
- Search/filter controls are still form-like rather than “quick filter” optimized.
- Empty-state quality is better than older pages, but the table view remains dense before interaction.
#### Suggested Improvements
- Collapse secondary row metadata into stacked cell layouts more aggressively on smaller breakpoints.
- Consider moving certificate preview into row details or action menu on mobile.
- Add a compact filter mode for narrow widths.
- Normalize hours formatting and right-alignment across all numeric columns.
#### Priority
- High
### Create Training Record
#### Strengths
- Form validation, required states, and permission messaging are present.
- Scroll-to-first-error behavior improves usability.
- Upload guidance and review status context are included.
#### Problems
- The form is long and visually uniform, making it hard to scan by section.
- Inputs, selects, upload, and custom combobox patterns feel mixed rather than part of one clearly grouped form system.
- The time-to-hours interaction is clever but not instantly obvious to first-time users.
- Supporting text competes with labels because spacing and hierarchy are similar everywhere.
#### Suggested Improvements
- Break the form into named sections such as participant, course, schedule, evidence, and notes.
- Add section separators or cards to reduce cognitive load.
- Improve time input affordance with inline example formatting or helper chips.
- Standardize textarea styling with shared form field components where possible.
#### Priority
- High
### Edit Training Record
#### Strengths
- Existing status and reviewer note context reduce ambiguity.
- Read-only guard for non-editable records is explicit.
#### Problems
- Edit mode and create mode share nearly the same visual treatment, so users get weak cues about record lifecycle.
- Read-only state depends on warning copy more than structural UI changes.
#### Suggested Improvements
- Add stronger edit-state framing with status banner treatment and section locking visuals.
- Visually disable non-editable zones instead of only relying on top-level alert copy.
#### Priority
- Medium
### Pending Review
#### Strengths
- Purpose is clear and the empty state is task-appropriate.
- Action column supports review workflow efficiently.
#### Problems
- The table is still the dominant interaction model on narrow screens despite workflow-oriented content.
- Search plus multiple filters can become visually busy and difficult to parse quickly.
#### Suggested Improvements
- Consider a card-list fallback or row-detail preview pattern for mobile.
- Group filters into primary and secondary tiers.
- Surface status totals above the table for queue triage.
#### Priority
- High
### Employee Directory
#### Strengths
- Search and faceted filters are relevant to the use case.
- Detail page contains a strong breadth of information and summary metrics.
#### Problems
- List view is highly column-dense for the typical HR scan workflow.
- Detail page combines summary, profile, K/S/A, and history into a long vertical stack with limited sectional differentiation.
- The detail history table still uses a page-local table pattern instead of the shared table primitive style.
#### Suggested Improvements
- Reprioritize columns for the list page around primary identity and status first.
- Convert the detail screen into clearer sections with stronger rhythm between summary and history.
- Migrate the detail history table to the shared table system for consistency.
#### Priority
- High
### Users
#### Strengths
- User creation/editing via sheet is efficient for admin workflows.
- Table and form share reusable infrastructure.
#### Problems
- Users screen remains visually English-first while much of the application is Thai-first.
- Sheet form is dense and can become cramped for long option labels.
- Table columns are more operational than people-centric, which reduces scan speed.
#### Suggested Improvements
- Normalize content language strategy for admin screens.
- Increase grouping inside the sheet form: identity, organization, role, access.
- Shorten or visually tier organizer/account metadata in the list.
#### Priority
- Medium
### Reports
#### Strengths
- Clear export intent and multiple report blocks are easy to understand.
- Summary cards and data tables create a useful reporting structure.
#### Problems
- Reports page is one of the densest screens in the app.
- Multiple large tables in sequence create fatigue and weak “at a glance” hierarchy.
- Filter UI duplicates the dashboard filter style rather than sharing a standard analytics filter bar.
- Table cards vary from core DataTable behavior and still feel visually separate from the rest of the system.
#### Suggested Improvements
- Add a report section navigator or tabs to reduce long-page fatigue.
- Convert filter/header treatment into a shared analytics toolbar.
- Tighten summary-card copy and increase visual hierarchy before tables.
- Align report tables more closely with DataTable spacing, badges, and empty states.
#### Priority
- High
## Global Design Issues
- Native `select` usage and custom filter bars coexist with shadcn form patterns, creating inconsistent control styling.
- Data tables are shared in many places, but several detail/report views still use page-local table markup.
- Mixed Thai and English labels reduce product voice consistency.
- Button hierarchy is inconsistent across admin screens: some pages lead with outline actions, others with filled primary actions without a clear pattern.
- Dashboard KPI cards use inconsistent supporting content density.
## Responsive Issues
- Dashboard chart areas likely become visually crowded on tablet layouts before they become technically broken.
- Long forms such as Training Record still feel mobile-heavy even without viewport overflow.
- Pending Review and Training Records remain table-first on mobile and would benefit from alternate compact layouts.
- Employee Detail history table still relies on a local scroll-area table pattern and may diverge from shared overflow fixes.
- Header space is limited for future growth; breadcrumbs, trigger, theme toggle, and notifications already consume most of the row.
## Accessibility Issues
- Some flows still rely heavily on visual layout rather than explicit grouping or headings.
- Mixed native and custom controls may create uneven keyboard and focus behavior across filters/forms.
- Dense dashboard and report layouts risk poor focus traversal experience.
- Language inconsistency may also affect assistive clarity if labels change idiom by screen.
- More icon-only affordances should be audited for consistent accessible names, especially action menus and future toolbar actions.
## Recommended Refactoring
- Build a shared analytics filter panel used by Overview and Reports.
- Build a shared sectioned form layout wrapper for long forms.
- Standardize detail-page summary cards into a reusable metric-strip component.
- Migrate page-local tables to the shared `Table` or `DataTable` conventions.
- Standardize badge semantics for status, type, and progress across features.
- Create a language/content guideline for Thai-first vs English admin terminology.
## Suggested Improvements
- Introduce a shared dashboard filter component with the same spacing, field height, and button grouping everywhere.
- Add responsive “compact row” patterns for operational tables.
- Use clearer section headers and dividers on long forms and detail pages.
- Simplify KPI cards by limiting each to one primary metric and one supporting hint.
- Standardize empty-state design across DataTable, reports, and detail history sections.
- Normalize numeric formatting and alignment in all summary/table/report surfaces.
## Refactoring Opportunities
- Shared filter-bar abstraction for Overview and Reports
- Shared admin metric card for Dashboard, Reports, Employee Detail
- Shared detail-history table wrapper
- Shared long-form section component
- Shared status badge map component for approval, active/inactive, target status
## UI Consistency Checklist
- Typography: Partial pass
- Buttons: Partial pass
- Cards: Partial pass
- Tables: Partial pass
- Dialogs: Pass
- Forms: Partial pass
- Filters: Needs improvement
- Pagination: Partial pass
- Search: Partial pass
## Final Score
- UI: 7.2/10
- UX: 6.9/10
- Responsive: 6.8/10
- Accessibility: 7.0/10
- Performance: 7.6/10
- Maintainability: 7.4/10
- Overall Readiness: 7.1/10
## Notes
- This audit is based on the current code structure and component implementation patterns reviewed in Sprint 6.12.
- Scores reflect product readiness for UI refinement work before Sprint 7, not business-logic completeness.