Blazor HRM Documentation
A comprehensive guide to the architecture, features, and development workflow of the Blazor HRM application.
1. Architecture Overview
Indotalent uses Vertical Slice Architecture (VSA) with ASP.NET Core Areas. Each feature lives in its own self-contained folder, including its Razor pages, components, CQRS handlers, validators, and API endpoints. This eliminates the need to jump between multiple projects when working on a single feature.
Key Architectural Decisions
| Aspect | Implementation |
|---|---|
| Architecture | Vertical Slice via ASP.NET Core Areas |
| Backend API | Minimal API (retained for HTTP clients, downloads, and OpenAPI) |
| CQRS | Plain handlers (no MediatR dependency) |
| Database | EF Core with multi-provider (InMemory / SQL Server / PostgreSQL) |
| Primary Keys | String (GUID) - no auto-increment |
| Soft Delete | IHasIsDeleted + global query filter |
| Audit | IHasAudit + auto-populated on SaveChanges |
| Validation | FluentValidation (server) + component-side Validate() (UX mirror) |
| Frontend | Blazor Web App (interactive server render mode) + MudBlazor components |
| UI data access | HandlerInvoker - fresh DI scope per call, circuit user seeded for audit fields |
| Auth | ASP.NET Core Identity + JWT with Refresh Token Rotation + Firebase SSO |
| Rate Limiting | System.Threading.RateLimiting - 4 policies |
| CORS | Config-gated via appsettings.json (CorsSettings:IsUsed) - disabled by default |
| Background Jobs | Hangfire with built-in dashboard |
2. Project Structure
The project is organized into ASP.NET Core Areas. Each area groups features by access level:
| Area | Purpose | Auth Required |
|---|---|---|
Areas/Public/ | Public-facing pages (Home, Privacy, Documentation) | No |
Areas/Identity/ | ASP.NET Core Identity Razor Pages (Login, Register, Manage) | Mixed |
Areas/Admin/ | Admin-only features (User, Role, Tax, Country, Currency, Auto Number Sequence, Company, Health Check, Serilog and System File) | Admin role |
Areas/Main/ | HR workspace (HR Dashboard, Employee, Branch, Department, Designation, Todo and Todo Dashboard) | Admin, Member or Main |
Areas/Leave/ | Leave requests, categories and balances | Admin, Member or Leave |
Areas/Payroll/ | Payroll runs with salary components and grades | Admin, Member or Payroll |
Areas/Performance/ | Evaluations, appraisals, promotions and transfers | Admin, Member or Performance |
Areas/AI/ | AI Chat assistant over the configured LLM provider | Admin or AI |
Areas/Blazor/ | Blazor shell: App.razor, Routes.razor, layouts, circuit services, shared components | N/A |
Areas/Components/ | Shared dialogs (standard delete confirmation, etc.) | N/A |
Feature Folder Convention (VSA)
Every feature follows this convention:
+-- Pages/
| +-- Index.razor
| +-- Create.razor
| +-- Edit.razor
| +-- Detail.razor
+-- Components/
| +-- {EntityName}ModuleInfo.razor
+-- Cqrs/
| +-- Get{EntityName}ListHandler.cs
| +-- Get{EntityName}ByIdHandler.cs
| +-- Create{EntityName}Handler.cs + Validator.cs
| +-- Update{EntityName}Handler.cs + Validator.cs
| +-- Delete{EntityName}Handler.cs
+-- Endpoints/{EntityName}Endpoint.cs
+-- {EntityName}Fields.cs (declarative FieldDescriptor sets)
Master-Detail features (like Areas/Main/Todo) add feature-local components
(Form, ItemsEditor, ItemDialog, Models.cs)
and hand-written pages instead of the descriptor-driven Fields.cs.
3. Application Name
The application name - displayed in the browser title bar, top-left brand, footer, and top navigation -
is configured centrally through appsettings.json. This allows you to rebrand the entire
application without editing any layout files manually.
| File / Path | Description |
|---|---|
Areas/Blazor/App.razor | Document shell: renders the app name in the browser title, navbar logo, and global assets |
Areas/Blazor/Layouts/MainLayout.razor | Authenticated shell: app bar, brand, WorkspaceTopNav, user menu, Module Information drawer |
appsettings.json → AppSettings | Central application name configuration |
Configure the application name in appsettings.json under AppSettings:
"AppSettings": {
"Name": "Blazor HRM"
}
To rebrand the application, simply change the "Name" value. The layouts read this value
at runtime via @Configuration["AppSettings:Name"], so the title, logo, and footer update
automatically across both the public area and the authenticated area layouts.
4. Functional Features
The application ships with 37 functional modules organized into seven areas: Main, Leave, Payroll, Performance, AI, Admin and Self Service. Each area below has a catalogue of its modules (route, type and purpose) followed by a detail card for every screen. The content mirrors the in-app Module Information drawer available on each screen, expanded with entity and workflow context from the source code.
Areas and required roles
| Area | Roles with access | Modules | Purpose |
|---|---|---|---|
| Main | Admin, Member, Main | 7 | HR workspace: dashboards, todos, employees and the branch/department/designation masters |
| Leave | Admin, Member, Leave | 3 | Leave requests, categories and balances |
| Payroll | Admin, Member, Payroll | 4 | Payroll runs with printable slips, salary components and grades |
| Performance | Admin, Member, Performance | 4 | Evaluations, appraisals, promotions and transfers |
| AI | Admin, Ai | 1 | Conversational assistant over the configured LLM provider |
| Admin | Admin | 10 | Identity, reference data, sequences, company profile and system utilities |
| Self Service | Guest (linked) | 8 | Employee self-service: own profile, leave, payslips and performance records |
| Total | 37 |
Access is enforced per page with [Authorize(Roles = ...)]. The Admin role can open every
area; each area then additionally admits its module role (for example Leave needs the Leave role, Main
accepts Member, Main or Admin). The navigation menu only shows the areas a signed-in
user holds roles for. See Authorization for the full role model.
Common list behavior
The module screens share a consistent UX, so the steps below are not repeated in every card:
- Every list is a server-side paged grid with a free-text search box that matches the columns noted per module.
- Clicking a row selects it and reveals the row actions (typically Detail and Edit).
- The header offers Create New and Export XLSX; the export honors the active search filter.
- The header-then-lines pattern applies to the three Master-Detail modules (Employee, Todo and Payroll Process): Employee salary components are maintained on the Edit page (Quick Add, inline editing, and Add/Edit/Detail dialogs), todo checklist items are managed on the Create, Edit and Detail screens (Quick Add and Add/Edit/Detail dialogs), and a Payroll Process run materializes its per-employee details when the period is created.
- Payroll slips are printed to PDF from the Payroll Process Detail page; lists export to XLSX instead.
- Deletes are soft deletes; where noted, deleting a master record also soft-deletes its child rows (an employee's salary components, a payroll run's details and components).
- Self Service screens are read-only except My Leave Requests (Create New); every query is scoped server-side to the signed-in employee.
- Auto-numbered records receive a stable reference from the shared sequence engine - see AutoNumber Generation.
Demo data included. All modules ship with seeded demo records (employees with salary components, branches, departments, designations, todos, leave categories, requests and balances, payroll runs, evaluations, appraisals, promotions and transfers) so every screen is populated on first run. See Demo Mode for how the seeding is toggled.
Module index
Jump directly to any module card:
Main: HR Dashboard · Employee · Branch · Department · Designation · Todo · Todo Dashboard
Leave: Leave Request · Leave Category · Leave Balance
Payroll: Payroll Process · Income · Deduction · Grade
Performance: Evaluation · Appraisal · Promotion · Transfer
AI: Chat
Admin: User · Role · Tax · Country · Currency · Auto Number Sequence · Company · Health Check · Serilog · System File
Self Service: My Profile · My Leave Requests · My Leave Balances · My Payslips · My Evaluations · My Appraisals · My Promotions · My Transfers
Main
The Main area is the HR workspace: the HR dashboard, the employee register and the branch, department and designation masters, plus the personal to-do modules carried over from the template. It is the default landing area after sign-in. Access requires Admin, Member or Main role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| HR Dashboard | /Main/HrmDashboard | Dashboard | Live people metrics across employees, branches, departments, leave, appraisals, promotions, transfers and the latest payroll run |
| Employee | /Main/Employee | Master-Detail | Employee register with salary components (income/deduction rows) |
| Branch | /Main/Branch | Master Data | Office branches referenced by employees and transfers |
| Department | /Main/Department | Master Data | Departments with a cost-center code, referenced by employees and transfers |
| Designation | /Main/Designation | Master Data | Job titles with a level, referenced by employees and transfers |
| Todo | /Main/Todo | Master-Detail | Personal tasks with checklist items, attachments, priority and progress |
| Todo Dashboard | /Main/TodoDashboard | Dashboard | Read-only analytics over the todo dataset |
HR Dashboard
/Main/HrmDashboard · Access: Admin, Member, Main · Dashboard
Aggregates live people metrics across employees, branches, departments, leave, appraisals, promotions, transfers and the latest payroll run. All numbers are computed from the same data the module screens maintain, so the dashboard never needs separate bookkeeping.
What you can do
- Track headcount, the FTE percentage (the share employed Full-Time), the average appraised salary, the latest payroll gross, pending leaves, active promotions and the branch count at a glance.
- Compare the six-month headcount trend against new hires per month, and review the department distribution and the leave type breakdown.
- Review top performers, recent transfers with their impact level, pending leave tickets, recent salary reviews, upcoming promotions, compensation composition, career benchmarks, recent appraisal records and the activity feed.
Key metrics
| Metric | Description |
|---|---|
| Headcount / FTE | Total employee records and the percentage employed Full-Time. |
| Average appraised salary | Average New Salary across appraisal records. |
| Latest payroll gross | Taken from the payroll run with the latest period end; the compensation bars show Take Home, Deductions and Incomes as percentages of gross. |
| Top performers | Built from the ten most recent evaluations; the KPI score uses the evaluation Final Score and bands map to Top Talent (>= 85), Core Player (60-84) and Low Performer (< 60). |
| Career benchmarks | Basic salary compared with the grade Salary To; rows are labelled Entry, Mid Range or Top Range. |
| Retention / Engagement | Constant indicators (94.8 and 74.2) preserved from the source application; they are not data-derived. |
Things worth attention
- Charts and tables are zero-data safe: they render empty states until the underlying modules have records.
- Department distribution includes departments with zero employees so gaps remain visible.
- Use Refresh to recompute every metric and redraw the charts.
Employee
/Main/Employee · Access: Admin, Member, Main · Master-Detail
The master-detail employee register. Each employee carries a full profile - personal data, assignment to a branch, department, designation and salary grade, salary-bank details, address and contact, social media and additional information - plus a set of salary components (employee income and employee deduction rows) that the payroll engine uses to compute monthly pay.
What you can do
- Browse, search and page through employee records (server-side grid); the Items column shows how many salary components the employee has.
- Use Create New to add an employee and Export XLSX to download the currently filtered list.
- Maintain salary components on the Edit page: add a row with Quick Add or the Add dialog, edit rows inline or in the dialog, or open the read-only detail dialog; every change saves immediately.
Key properties
| Property | Description |
|---|---|
| Profile panels | Main, Assignment, Personal, Salary Bank, Address & Contact, Social Media and Additional. |
| Name | First and last name are required; the middle name is optional. |
| Required fields | Email, identity number, joined date, branch, department, designation, salary grade, gender and phone; basic salary must be greater than zero on create. |
| Code | Left blank on create it is back-filled from the auto number; required on edit. |
| Salary components | Each row stores a Kind (Income or Deduction), the active Income/Deduction master, an amount, a description and Is Active; rows save immediately. |
| System User | Optional link to an active, unlinked Identity user. Saving the link automatically grants the Guest role so the account can open the Self Service area; clearing the field unlinks (the role stays). A user can be linked to only one employee. |
| Employment | Status (Active/Inactive/Resigned) and employment type; only Active employees are picked up by the payroll engine. |
Things worth attention
- Search matches Code, first/middle/last name, email, phone, job description, status, employment type, gender or marital status.
- Deleting an employee is a soft delete that also soft-deletes the salary component rows.
- Leave requests, leave balances, payroll results and the performance modules all reference employees.
Branch
/Main/Branch · Access: Admin, Member, Main · Master Data
A CRUD screen for the organization's office branches. Each record stores an auto number, a short unique code, a name, an address block, contact details and up to three free-form other-information fields. Employees are assigned to a branch, and Transfers record movement between branches.
What you can do
- Browse, search and page through branch records (server-side grid).
- Use Create New to add a branch and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Code | Short unique code. If left blank on create, it is back-filled from the auto number; required on edit. |
| Name / Address | Name, street address and city are required; state/province and zip code are optional. |
| Contact | Phone and email are optional; the email must be a valid address when filled. |
Things worth attention
- Search matches Code, Name, Description, City, Phone or Email.
- The Employee form and the Transfer form both use the branch list as a lookup.
- Deleting a branch is a soft delete and does not delete its employees or transfers.
Department
/Main/Department · Access: Admin, Member, Main · Master Data
A CRUD screen for the organization's departments. Each record stores an auto number, a cost-center code, a name, an optional description, the head of department and up to three free-form other-information fields. Employees are assigned to a department, and Transfers record movement between departments.
What you can do
- Browse, search and page through department records (server-side grid).
- Use Create New to add a department and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Cost Center | Unique cost-center code. If left blank on create, it is back-filled from the auto number; required on edit. |
| Name | Required, up to 100 characters. |
| Head of Department | Optional free text naming the department head. |
Things worth attention
- Search matches Cost Center, Name, Description or Head of Department.
- The Employee form and the Transfer form both use the department list as a lookup.
- Deleting a department is a soft delete and does not delete its employees or transfers.
Designation
/Main/Designation · Access: Admin, Member, Main · Master Data
A CRUD screen for job titles and positions. Each record stores an auto number, a short code, a name, a level (for example Executive, Management or Professional), an optional description and up to three free-form other-information fields. Employees are assigned to a designation, and Transfers record movement between designations.
What you can do
- Browse, search and page through designation records (server-side grid).
- Use Create New to add a designation and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Code | Short unique code. If left blank on create, it is back-filled from the auto number; required on edit. |
| Name / Level | Both required; the level groups designations by seniority. |
| Description | Optional. |
Things worth attention
- Search matches Code, Name, Description or Level.
- The Employee form and the Transfer form both use the designation list as a lookup.
- Deleting a designation is a soft delete and does not delete its employees or transfers.
Todo
/Main/Todo · Access: Admin, Member · Master-Detail
The template's personal task-management module, carried over unchanged: each to-do captures a title, optional description, priority (Low/Medium/High), category (Personal/Work/Learning), a due date and time, numeric progress, a completed flag and an optional owner. A to-do is a master record that holds child checklist items as well as image and file attachments.
What you can do
- Browse, search, sort and page through to-dos (server-side grid); use Create New and Export XLSX as in every list.
- Manage checklist items and attachments on the Create, Edit and Detail screens.
Things worth attention
- Status is derived solely from the completed flag and is independent of the numeric progress; update both intentionally.
- The Todo Dashboard provides a read-only analytics view over this same dataset.
Todo Dashboard
/Main/TodoDashboard · Access: Admin, Member · Dashboard
The template's read-only analytics view over the whole to-do dataset: KPI widgets (Total Tasks, Completed, Pending, Overdue), priority and category charts, a six-month task flow and the ten latest to-dos. Everything is built from a single payload, so the widgets, charts and table always agree on totals.
Things worth attention
- The dashboard is read-only; create, edit, complete and delete actions live in the Todo module.
- Soft-deleted records are excluded by the global query filter and never appear in any metric.
- Refresh re-fetches the payload without reloading the page.
Leave
The Leave area covers employee leave applications, the categories that define how leave is granted, and the per-employee balances that track entitlements. Access requires Admin, Member or Leave role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Leave Request | /Leave/LeaveRequest | Master Data | Employee leave applications with one optional attachment |
| Leave Category | /Leave/LeaveCategory | Master Data | How leave is granted: quota, paid-leave and carry-forward rules |
| Leave Balance | /Leave/LeaveBalance | Master Data | Per-employee, per-category yearly balances with a Sync Balances action |
Leave Request
/Leave/LeaveRequest · Access: Admin, Member, Leave · Master Data
Records employee leave applications. Each request pairs an employee with a leave category, a start and end date and an auto-derived day count, and carries a reason, a status, an optional single attachment and the approval outcome.
What you can do
- Browse, search and page through leave requests (server-side grid).
- Use Create New to record a request and Export XLSX to download the currently filtered list.
- Review requests on the Approval panel: set the status, rejection reason and action date.
- Upload one attachment from the Edit page; a new upload replaces the previous file.
Key properties
| Property | Description |
|---|---|
| Employee / Category | Both required; employees come from the active employee list, categories from the Leave Category master. |
| Dates / Days | Start and end date are required and the end date must not precede the start; the total days are calculated automatically from the range and re-derived again on save. |
| Status | Pending, Approved, Rejected or Cancelled. Approved days feed the Used column of Leave Balances; Pending days feed the Pending column (via the sync action on the Leave Balance list). |
| Attachment | One optional file (PDF, DOC, DOCX, PNG, JPG, JPEG, ZIP; max 5MB) uploaded from the Edit page, replacing the previous file. |
| Approval | The rejection reason and action date are recorded on the Approval panel when the request leaves the Pending state. |
Things worth attention
- Search matches employee name/code, leave category, reason, reject reason or status.
- Deleting a request is a soft delete that also removes the stored attachment file.
- Leave balances are updated by the explicit Sync Balances action on the Leave Balance list, not automatically on approval.
Leave Category
/Leave/LeaveCategory · Access: Admin, Member, Leave · Master Data
A Leave-area CRUD screen for leave category records that define how leave is granted (for example, annual leave, sick leave or unpaid leave). Each record stores an auto number, a unique code, a name, a yearly quota, paid-leave and carry-forward settings, a minimum service requirement and a status.
What you can do
- Browse, search and page through leave category records (server-side grid).
- Use Create New to add a leave category and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Code | Short unique code. If left blank on create, it is back-filled from the auto number; required on edit. |
| Name | Required, up to 100 characters. |
| Quota | Default entitlement in days per period; 0 is allowed. |
| Paid Leave | Whether leave taken under this category is paid. |
| Carry Forward | Whether unused quota may be carried into the next period, and the maximum days carried (Allow Carry Forward / Max Carry Forward). |
| Min Service Months | Minimum service months required before leave can be taken under this category. |
| Status | Active or Inactive; shown as a chip in the grid. |
Things worth attention
- Search matches Code, Name, Description or Status.
- Leave Category records are referenced by Leave Request and Leave Balance records.
- Deleting a leave category is a soft delete; the record disappears from the grid but is not physically removed.
Leave Balance
/Leave/LeaveBalance · Access: Admin, Member, Leave · Master Data
Tracks how much leave each employee is entitled to per category and year. Each row stores an entitlement, a carry-forward allowance and the computed Used, Pending and Remaining counters, plus the validity window of the balance period.
What you can do
- Browse, search and page through leave balance rows (server-side grid).
- Use Create New to add a balance row, Sync Balances to recalculate the counters, and Export XLSX to download the filtered list.
Key properties
| Property | Description |
|---|---|
| Employee / Category / Year | All required; a row is unique per employee, category and year in practice. |
| Entitlement / Carry Forward | Manually maintained allowances; both must be zero or greater. |
| Used / Pending / Remaining | Computed by the Sync Balances action: Used is the sum of Approved leave request days for the same employee, category and balance year; Pending is the sum of Pending days; Remaining = Entitlement + Carry Forward - Used. |
| Remaining chip | Turns amber at zero and red when negative. |
| Valid From / Valid To | Optional validity window of the balance period. |
Things worth attention
- Search matches employee name/code, leave category or a numeric year.
- Approving or rejecting a leave request does not change the balance automatically; run Sync Balances after review activity.
- Editing a row recalculates Remaining from Entitlement + Carry Forward - Used without touching Used or Pending.
Payroll
The Payroll area runs and archives the monthly payroll and maintains the income, deduction and grade masters that feed it. Creating a run snapshots every active employee into per-employee details with printable slips. Access requires Admin, Member or Payroll role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Payroll Process | /Payroll/PayrollProcess | Master-Detail | Payroll run with per-employee details and printable payroll slips |
| Income | /Payroll/Income | Master Data | Reusable income components attached to employees |
| Deduction | /Payroll/Deduction | Master Data | Deduction components attached to employees |
| Grade | /Payroll/Grade | Master Data | Salary grades with bands, referenced by employees and promotions |
Payroll Process
/Payroll/PayrollProcess · Access: Admin, Member, Payroll · Master-Detail
Runs and archives monthly payroll. Creating a run immediately snapshots every ACTIVE employee into a per-employee detail row (basic salary, income total, deduction total and take-home pay) with the full income and deduction component breakdown, and stores the process totals for the run.
What you can do
- Browse, search and page through payroll runs (server-side grid).
- Use Create New to run a new period: the payroll engine immediately materializes the details and totals.
- Open Detail to inspect the per-employee results and print a PDF payroll slip per employee, and Edit to update the run, change its status, or re-run the calculation.
Key properties
| Property | Description |
|---|---|
| Period Name | Required and unique; a second run with the same period name is rejected. |
| Period Dates | From/To dates are required on create; the To date must not precede the From date. |
| Details | One row per active employee: basic salary, income total, deduction total and take home (basic + income - deduction), with the component breakdown per income/deduction row; component names come from the Income/Deduction masters. |
| Totals | Employees, basic salary, income, deduction, gross (basic + income) and take home (gross - deduction), computed by the payroll engine. |
| Status | Draft, Processed or Paid; the first move to Processed stamps the processing user and time. |
Things worth attention
- Recalculate on the Edit page soft-deletes the previous details and components and re-materializes them from live data.
- The Detail page shows the run, the totals and a per-employee slip popup (identity, basic salary, income lines, deduction lines and take home) that is printed to PDF from the browser without a server-side PDF library.
- Deleting a run is a soft delete that also removes its details and components.
- Search matches Period Name, Description, Processed By or Status.
Income
/Payroll/Income · Access: Admin, Member, Payroll · Master Data
A payroll CRUD screen for reusable income components (for example, fixed allowances or variable bonuses). Each component stores an auto number, a unique code, a name, a type, a taxable flag, a calculation base and a status, and is attached to employees through the salary component rows on the Employee Edit page.
What you can do
- Browse, search and page through income records (server-side grid).
- Use Create New to add an income component and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Code | Short unique code. If left blank on create, it is back-filled from the auto number. |
| Name | Required, up to 100 characters. |
| Type | Fixed or Variable, describing whether the amount stays constant or varies. Required. |
| Taxable | Yes/no flag marking whether the income is subject to tax. |
| Calculation Base | Required; describes what the amount is based on. |
| Status | Active or Inactive; an inactive component is kept for reference but treated as retired. |
Things worth attention
- Search matches Code, Name, Description, Calculation Base, Type or Status.
- The active income masters feed the Employee salary component picker and are included by the payroll engine.
- Deleting an income component is a soft delete; the record is flagged as deleted instead of being removed.
Deduction
/Payroll/Deduction · Access: Admin, Member, Payroll · Master Data
A payroll CRUD screen for the deduction components used across the application. Each record stores an auto number, a unique code, a name, a category, a pre-tax flag, a calculation method and a status, and is attached to employees through the salary component rows on the Employee Edit page.
What you can do
- Browse, search and page through deduction records (server-side grid).
- Use Create New to add a deduction and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Code | Short unique code. If left blank on create, it is back-filled from the auto number; required on edit. |
| Name | Required, up to 100 characters. |
| Category | Groups the deduction: Statutory, Retirement, Insurance, Loan, Penalty or Internal. |
| Pre Tax | Whether the deduction is applied before tax is calculated. |
| Calculation Method | Required; describes how the deduction amount is computed. |
| Status | Active or Inactive; only active deductions are offered to the payroll engine. |
Things worth attention
- Search matches Code, Name, Description, Calculation Method, Category or Status.
- Employee salary component rows reference this list, and the payroll engine consumes the deduction during payroll processing.
- Deleting a deduction is a soft delete; existing component rows are not removed.
Grade
/Payroll/Grade · Access: Admin, Member, Payroll · Master Data
A payroll CRUD screen for salary grades and levels. Each record stores an auto number, a unique code, a name, a salary band (Salary From / Salary To), an over-time eligible flag and a status. Grades are assigned to employees and referenced by promotions.
What you can do
- Browse, search and page through grade records (server-side grid).
- Use Create New to add a grade and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Code | Short unique code. If left blank on create, it is back-filled from the auto number. |
| Name | Required, up to 100 characters. |
| Salary From / To | The salary band for the grade; Salary To must be greater than or equal to Salary From. |
| Over Time Eligible | Yes/no flag marking whether employees in this grade qualify for overtime. |
| Status | Active or Inactive; an inactive grade is kept for reference but treated as retired. |
Things worth attention
- Grades are assigned to employees and referenced by Promotion From Grade / To Grade.
- Search matches Code, Name, Description or Status.
- Deleting a grade is a soft delete; employees and promotions keep their references.
Performance
The Performance area records the people events around employees: evaluations, salary appraisals, grade promotions and organization transfers. All four modules reference the employee, grade, branch, department and designation masters. Access requires Admin, Member or Performance role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Evaluation | /Performance/Evaluation | With Lookup | Employee evaluations with evaluator, period and final score |
| Appraisal | /Performance/Appraisal | With Lookup | Salary appraisals with current/new salary and increment percentage |
| Promotion | /Performance/Promotion | With Lookup | Grade moves from a From Grade to a To Grade |
| Transfer | /Performance/Transfer | With Lookup | Branch, department and designation moves |
Evaluation
/Performance/Evaluation · Access: Admin, Member, Performance · With Lookup
A performance CRUD screen for employee evaluations. Each record links one employee to an evaluator (also an employee), covers a review period and stores the evaluation date, a final score, an optional rating and notes, plus a workflow status.
What you can do
- Browse, search and page through evaluation records (server-side grid).
- Use Create New to add an evaluation and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Employee | Required; the employee being evaluated, chosen from active employees. |
| Evaluator | Required; the employee who performs the evaluation, also chosen from active employees. |
| Period | Required, up to 100 characters; describes the review window the evaluation covers. |
| Evaluation Date | Required calendar date of the evaluation. |
| Final Score | Required, a 0-100 percent score for the period, shown with two decimals. |
| Rating | Optional free text, up to 50 characters. |
| Status | Workflow state: Draft, Reviewing or Completed. |
| Evaluation Note | Optional notes, up to 500 characters. |
Things worth attention
- Search matches Period, Employee, Evaluator, Rating or Status.
- Both the employee and the evaluator come from active employees.
- The status workflow moves from Draft to Reviewing to Completed.
- Deleting an evaluation is a soft delete; the record is flagged as deleted instead of being removed.
Appraisal
/Performance/Appraisal · Access: Admin, Member, Performance · With Lookup
A performance CRUD screen for employee salary appraisals. Each record belongs to one employee and stores the Current Salary, the Increment Percentage and the resulting New Salary, plus an appraisal type, an effective date, an optional last rating, notes and a workflow status.
What you can do
- Browse, search and page through appraisal records (server-side grid).
- Use Create New to add an appraisal and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Employee | Required; the employee the salary appraisal applies to, chosen from active employees. |
| Appraisal Type | Annual Review, Promotion, Special Adjustment or Annual Merit Increase. |
| Current Salary | The employee salary before the appraisal; required and 0 or greater. |
| Increment Percentage | The salary increment percentage between 0 and 100, shown with two decimals. |
| New Salary | The salary after the increment is applied; 0 or greater. |
| Effective Date | Required calendar date the new salary takes effect. |
| Last Rating | Optional free text carried over from a previous evaluation, up to 50 characters. |
| Status | Workflow state: Pending, Approved or Processed. |
| Appraisal Note | Optional notes, up to 500 characters. |
Things worth attention
- Search matches Employee, Appraisal Type, Rating or Status.
- The status workflow moves from Pending to Approved to Processed.
- Current Salary, Increment Percentage and New Salary must be 0 or greater; the increment percentage is capped at 100.
- Last Rating is free text from a previous evaluation - it is stored as entered and never recalculated.
Promotion
/Performance/Promotion · Access: Admin, Member, Performance · With Lookup
Records an employee moving from a From Grade to a To Grade effective on a given date. Each record links an employee, an optional previous grade, the target grade and a status that tracks whether the promotion is still pending, confirmed or canceled.
What you can do
- Browse, search and page through promotion records (server-side grid).
- Use Create New to add a promotion and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Employee | Required; the employee being promoted, picked from the active employee list. |
| From Grade | Optional; the employee's previous grade. It may be left blank when the employee has no recorded grade. |
| To Grade | Required; the target grade the employee is moving to. |
| Effective Date | Required; the date the promotion takes effect. |
| Status | Pending by default; later moved to Confirmed or Canceled. |
| Promotion Note | Optional, up to 500 characters. |
Things worth attention
- Search matches Employee, From Grade, To Grade or Status.
- Grades come from the Payroll Grade master; From Grade is optional, To Grade is always required.
- Status starts as Pending and is later moved to Confirmed or Canceled.
- Deleting a promotion record is a soft delete.
Transfer
/Performance/Transfer · Access: Admin, Member, Performance · With Lookup
A performance CRUD screen for employee transfers. Each record moves an employee from a From branch, department and designation to a To branch, department and designation, effective on a transfer date.
What you can do
- Browse, search and page through transfer records (server-side grid).
- Use Create New to record a transfer and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Employee | Required; chosen from the active employees. |
| From Branch / Department / Designation | Optional; the current placement of the employee. |
| To Branch / Department / Designation | The destination placement. To Branch and To Department are required; To Designation is optional. |
| Transfer Date | Required; the effective date of the move. |
| Status | Progress of the record: Pending, then Approved, then Processed. Defaults to Pending. |
| Transfer Note | Optional notes, up to 500 characters. |
Things worth attention
- Search matches Employee, Branch, Department, Designation or Status.
- Branch, Department and Designation options come from the Main masters (Branch, Department and Designation).
- Deleting a transfer from the Detail page is a soft delete; the record is hidden from the list but kept in the database.
AI
The AI area contains a single module: a conversational assistant backed by the configured LLM provider. Access requires the Admin or Ai role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Chat | /AI/Chat | Interactive | Conversational assistant with markdown replies from the configured provider |
AI Chat
/AI/Chat · Access: Admin, Ai · Interactive
A conversational assistant screen. A signed-in user types a message and receives a single reply from the configured LLM provider; replies render as formatted markdown.
What you can do
- Send a message and receive a single reply from the configured AI provider.
- Your messages appear as indigo bubbles on the right; assistant replies appear as white bubbles on the left.
- While the full answer is awaited, the assistant bubble shows an "AI is thinking..." spinner; failures surface in a red alert banner above the input area.
Key facts
- Provider, model and API keys come from AiSettings in appsettings.json - see AI Chat configuration. Supported providers are ChatGPT, Claude, Gemini and DeepSeek; the committed default provider is ChatGPT.
- The request flow is single-turn: only the new message is sent per request - there is no server-side multi-turn context.
- Markdown is rendered client-side (headings, lists, bold, links, blockquotes, inline code, dark code blocks, tables, horizontal rules and images).
- Conversation history is kept in memory only - reloading the page resets it; nothing is persisted.
Things worth attention
- The renderer is markdown-friendly, so asking for lists, tables or code produces nicely formatted replies.
- Which models are actually reachable depends on the configured API keys.
Admin
The Admin area is the administrative backbone: identity and role management, reference data (tax, countries, currencies), the auto-number engine, the company profile, and system utilities for monitoring, logs and deployment files. All screens require the Admin role, and the area appears in the menu only for accounts that hold both the Member and Admin roles.
| Module | Route | Type | Purpose |
|---|---|---|---|
| User | /Admin/User | Identity | Sign-in accounts, roles, passwords and profile parameters |
| Role | /Admin/Role | Identity | Read-only list of the application roles |
| Tax | /Admin/Tax | Master Data | Standalone tax-rate reference data (percentage lookups) |
| Country | /Admin/Country | Master Data | Country reference data |
| Currency | /Admin/Currency | With Lookup | Currencies linked to countries |
| Auto Number Sequence | /Admin/AutoNumberSequence | Configuration | Per-entity counters behind every auto number |
| Company | /Admin/Company | Configuration | Single-record organization profile |
| Health Check | /Admin/HealthCheck | Dashboard | Health probes plus in-memory traffic metrics |
| Serilog | /Admin/Serilog | Utility | Browse, read and download the rolling log files |
| System File | /Admin/SystemFile | Utility | Read-only views of App Settings, Dockerfile and Docker Compose |
User
/Admin/User · Access: Admin · Identity
Manages the application's sign-in accounts (ASP.NET Identity records): username, e-mail, full name, avatar, e-mail confirmation state, assigned roles, password and profile parameters. It is an admin-only module.
What you can do
- Browse the paged, searchable list of accounts (server-side grid).
- Click a row to reveal the row actions: Detail, Edit, Roles, Avatar, Password, Email and Params.
- Use Create New to add an account and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Avatar | Profile picture rendered from the wwwroot/data/avatar folder; empty values show a gray placeholder. |
| Username | The Identity login name; unique per account. |
| Sign-in e-mail; the app requires a confirmed account at sign-in. | |
| Full Name | Display name used across the UI. |
| Email Confirmed | Shown as Confirmed (green) or Not Confirmed (red); toggled through the Email action. |
| Audit | Created At/By and Updated At/By tracking; deleted users are soft-deleted and hidden from lists. |
Things worth attention
- Search matches Username, Email or Full Name only.
- Create New makes the account pre-confirmed and active, and returns a randomly generated 12-character password to hand over securely.
- Role assignment replaces the user's whole role set. The default administrator (admin@root.com) is protected from role, e-mail and password changes in demo mode.
- Params edits personal info, address, social media and "other information" stored directly on the user record.
- Menu visibility depends on the roles a user holds; see Authorization.
Role
/Admin/Role · Access: Admin · Identity
A read-only list of the application's roles. Roles gate which modules and menus appear: the Main area is available to Member/Admin, the Admin area requires Member and Admin together, and AI requires Admin or AI.
What you can do
- Browse and search roles (search matches the role name); rows are always ordered A-Z.
- Open the Detail view for a role and export the list to XLSX.
Things worth attention
- There is no create, edit or delete path in this module - it is browse-and-detail only.
- Eight roles are seeded: Guest, Member, Admin, AI, Main, Leave, Payroll and Performance. Guest is the employee self-service login: linking a user to an employee record (Employee → System User) grants it and unlocks the Self Service area.
- A user's role set is managed from Admin → User → Roles, where assignment replaces the whole set.
Tax
/Admin/Tax · Access: Admin · Master Data
An admin CRUD screen for tax-rate lookups (percentages) used as standalone reference/master data. Each record carries an auto number, a unique code, a name, a percentage and an optional description; no other HRM module consumes these records today.
What you can do
- Browse, search and page through tax records (server-side grid).
- Use Create New to add a tax and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference (for example a TAX/2026/0001-style number). |
| Code | Short unique code; if left blank on create it is back-filled from the generated auto number. |
| Name | Required, up to 100 characters. |
| Percentage | Rate between 0 and 100, shown as a pill with a trailing % sign. |
| Description | Optional free text. |
Things worth attention
- Search matches Code or Name; codes must be unique.
- Tax is standalone reference data: it is not consumed by the HRM payroll, leave or performance modules.
Country
/Admin/Country · Access: Admin · Master Data
An admin CRUD screen for country lookup records used as reference data (for example, the Currency module links to countries). Each record stores an auto number, a unique code, a name and an optional description.
What you can do
- Browse, search and page through country records (server-side grid).
- Use Create New to add a country and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Code | Short unique code (backed by a unique index); back-filled from the auto number when left blank on create. |
| Name | Required, up to 100 characters. |
| Description | Optional free text. |
Things worth attention
- Search matches Code, Name and Description.
- Countries are seeded: US, EU, GB, ID, SG, IN, AU and CN.
- The Currency module references these records.
Currency
/Admin/Currency · Access: Admin · With Lookup
An admin CRUD screen for currencies, each optionally linked to a country. Records carry an auto number, a unique code, a name, a symbol, an optional country reference and a description.
What you can do
- Browse, search and page through currency records (server-side grid).
- Use Create New to add a currency and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Code | Short unique code; back-filled from the auto number when left blank on create. |
| Name | Required, up to 100 characters. |
| Symbol | Required symbol, up to 5 characters (for example $, € or IDR-style text). |
| Country | Optional Country lookup; the grid shows the country name or an em dash. |
| Description | Optional free text. |
Things worth attention
- Search matches Code, Name or the linked Country name; codes must be unique.
- Seeded currencies correspond to the seeded countries: USD, EUR, GBP, IDR, SGD, INR, AUD and CNY.
- Companies reference currencies; the Company profile requires one.
Auto Number Sequence
/Admin/AutoNumberSequence · Access: Admin · Configuration
A maintenance screen for the internal per-entity, per-year counters that generate the stable Auto Number values on auto-numbered records (tax, country, currency, company and more). Each row describes one entity/year bucket.
What you can do
- Browse the counters and open Detail or Edit to inspect or adjust a sequence.
- Use Create New to add a sequence manually; rows are normally created automatically on first use.
- Export the list to XLSX.
Key properties
| Property | Description |
|---|---|
| Entity Name | The record type the sequence feeds (for example Tax, Country, Currency or Company). |
| Year / Month | Counter period; the current implementation tracks per year only (Month is empty). |
| Current Sequence | The last issued value; incremented every time a new record is created. |
| Prefix / Suffix Template | Text around the padded number; may substitute placeholders such as {EntityName}, {Year}, {Year2} and {Month}. |
| Padding Length | Zero-padding width (default 4), so value 1 renders as 0001. |
How it works
When a new auto-numbered record is saved without an Auto Number, the system derives a short code from the entity name (first three consonants, uppercased), forms a prefix template such as TAX/2026/, looks up (or lazily creates) the matching sequence row, increments Current Sequence, and formats prefix + padded value + suffix. Editing a row affects only numbers generated afterward. See AutoNumber Generation for the runtime flow.
Things worth attention
- Search matches Entity Name only.
- Do not lower Current Sequence while records are still being created, or generated numbers may collide.
- Rows exist only for entity/year combinations that have actually issued a number.
Company
/Admin/Company · Access: Admin · Configuration
The organization master record used across the app. The UI is effectively single-record: the data model allows many rows but there is no create path, and the seeded default record is Acme Global Solutions Inc., flagged as default and set to the seeded USD currency.
What you can do
- Locate the company record in the searchable list.
- Use Detail and Edit to update the profile data; Export XLSX downloads the list.
Key properties
| Property | Description |
|---|---|
| Auto Number / Name | Sequence-generated reference and required company name (up to 200 characters). |
| Currency | Required Currency lookup; the grid shows the currency name. |
| Tax Identification | Tax ID / registration number shown in the grid. |
| Business License / Description / Logo | Additional profile fields available on Detail and Edit. |
| Address | Street, city, state/province and zip code; the grid shows only the city. |
| Contact | Phone and e-mail. |
| Social / Other | Social media links (LinkedIn, X, Facebook, Instagram, TikTok) plus Other Information 1-3. |
| Default | The seeded company is flagged as default; the flag is not changeable through the API. |
Things worth attention
- Search matches Name, Tax ID, City, Phone or E-mail (not the currency name).
- There is no Create or Delete path in this module.
Health Check
/Admin/HealthCheck · Access: Admin · Dashboard
The System Monitoring Dashboard queries the public /health endpoint (overall banner plus one
card per registered check) and pulls an in-memory traffic snapshot from the Admin-only
/api/ops/metrics endpoint. See Health Checks
and System Monitoring for the underlying endpoints.
Health checks and traffic metrics
- Database - can the app connect to its database via EF Core.
- Storage - the wwwroot/data folder exists and has free space; under 100 MB free is reported as Degraded. Both checks are toggleable under HealthCheckSettings.
- Traffic is counted in memory by middleware; health probes, static assets and the metrics endpoint itself are excluded.
- The rolling window (default 5 minutes) and recent-request cap (default 200) are configurable under OpsMetricsSettings.
- Latency percentiles (P95/P99) are approximated from a bounded per-minute histogram, so memory stays flat under load.
Screen elements
- Overall status banner (Healthy, Degraded, Unhealthy or Status Unknown on fetch failure).
- One card per check with colored status badge, duration and description; an application card with app name, version, environment, uptime and server time.
- Traffic card with request count, 2xx/5xx counts, error rate, avg/P95/P99 latency and per-status-family counts.
- Recent endpoints list with method, path, status, duration and time; slow calls (1 second or more) are highlighted.
- Refresh reloads the page to re-run the checks and pull a fresh metrics snapshot.
Things worth attention
- If the traffic section reports an access error, the signed-in account is not an administrator.
- Traffic numbers reset whenever the web process restarts (the store is in-memory only).
Serilog
/Admin/Serilog · Access: Admin · Utility
An admin UI for the application's Serilog text log files (rolling daily), stored under
wwwroot/data/serilog with the file pattern log-yyyyMMdd.txt. Logging records
Information and above, and the newest file is listed first. See
Logging for the pipeline configuration.
What you can do
- Browse the log files and search by file name.
- Select a row to reveal Download (save the file) and Detail (read its content).
- Export the file list to XLSX.
Key properties
| Property | Description |
|---|---|
| File Name | Today's file is tagged with an Active badge because it is currently being written. |
| Size | Server-formatted size shown as a pill (B/KB/MB/GB). |
| Last Modified | Shown as yyyy-MM-dd HH:mm:ss. |
| Detail page | Reads the file in windows (default 500 lines, up to 2000) with Load Older / Load Newer, refresh, and Delete for past files; today's active file cannot be deleted. |
Things worth attention
- File cleanup is automatic: a daily background job deletes log files older than 3 days - export or download anything you need before cleanup runs.
- Each line follows the configured output template and starts with a timestamp and level.
System File
/Admin/SystemFile · Access: Admin · Utility
A read-only viewer for deployment and configuration files served from the application content root: App Settings (appsettings.json), Dockerfile and Docker Compose (docker-compose.yaml). Nothing here can be edited. See Application Name for how appsettings.json drives the UI, and Multi-Database for the connection strings shown in these files.
What you can do
- Open the three screens: App Settings (
/Admin/SystemFile/AppSettings), Dockerfile (/Admin/SystemFile/Dockerfile) and Docker Compose (/Admin/SystemFile/DockerCompose). - Read each file with sensitive values masked by default: SecretKey, Password, ApiKey, ApiSecret, ConnectionString, MessagingSenderId and AppId for App Settings; the database connection string and SMTP password for Docker Compose.
- Use Reveal (App Settings and Docker Compose only) to show raw values after a confirmation prompt, Mask Again to hide them, Copy to copy the displayed content, and Refresh to re-read the file from disk.
Things worth attention
- When demo mode is active, a reveal request is accepted but the server still returns masked content, so raw secrets are never sent.
- Files are resolved from a fixed whitelist; no path can be supplied by the client.
- Responses are sent with Cache-Control: no-store so secrets are not cached by the browser.
Hangfire dashboard. Besides the module screens, the Admin
menu links to the external Hangfire dashboard at /hangfire, which is mounted outside the VSA
module pattern and therefore has no Module Info drawer. See
Background Jobs for the scheduled jobs it manages.
Self Service (Guest)
The Self Service area is the employee-facing persona of the application. An HR administrator links a system user to an employee record (Employee → System User field), which automatically grants that account the Guest role. The employee then signs in and sees only their own HR data through eight read-only modules — plus one write path: submitting a new leave request. Every list, detail, item lookup and XLSX export is scoped server-side to the signed-in user, so records of other employees are never exposed.
| Module | Route | Type | Purpose |
|---|---|---|---|
| My Profile | /SelfService/Employee | Read-only | Own employee profile plus read-only salary components |
| My Leave Requests | /SelfService/LeaveRequest | Read-only + Create | Own leave requests; submitting always creates a Pending request |
| My Leave Balances | /SelfService/LeaveBalance | Read-only | Own entitlement, used, pending and remaining days per category and year |
| My Payslips | /SelfService/PayrollDetail | Read-only | Own payroll rows per period with a printable slip; Draft runs excluded |
| My Evaluations | /SelfService/Evaluation | Read-only | Own performance evaluations per period |
| My Appraisals | /SelfService/Appraisal | Read-only | Own salary appraisals with current/new salary and status |
| My Promotions | /SelfService/Promotion | Read-only | Own promotions with from/to grade and effective date |
| My Transfers | /SelfService/Transfer | Read-only | Own transfers with origin and destination assignment |
Access is enforced per page with [Authorize(Roles = "Guest")] and per endpoint group under
/api/selfservice/* with the same role. The sidebar shows the group above the Main group, and the
post-login Modules page offers the Self Service Module card to Guest accounts.
My Profile
/SelfService/Employee · Access: Guest (linked) · Read-only
The Self Service view of your own employee record: the same profile panels HR maintains (Main, Assignment, Personal, Salary Bank, Address & Contact, Social Media, Additional) plus a read-only salary-component table. Editing is exclusively an HR action from the Main area.
What you can do
- Browse your profile record (server-side grid) and open the Detail page with the full field set and your salary components.
- Export your profile to XLSX with the currently filtered values.
Things worth attention
- Only the employee record linked to your account is visible; an unlinked Guest sees the empty-state guidance.
- Salary components (income and deduction rows) are read-only here.
My Leave Requests
/SelfService/LeaveRequest · Access: Guest (linked) · Read-only + Create
The list of leave requests submitted for your employee record, with the category, date range, total days, status, and the approval outcome (action date and reject reason). This is the only Self Service module with a write path: Create New submits a request.
What you can do
- Submit a new request by picking a leave category, start/end dates and a reason; total days are derived from the dates.
- Track status changes (Pending → Approved/Rejected/Cancelled) and export the list to XLSX.
Things worth attention
- The employee is resolved server-side from your signed-in account — the form has no employee picker.
- New requests are always created with the Pending status: there is no approver field and no approve action here.
- Submitting does not change your leave balance until HR runs the balance sync after the decision.
My Leave Balances
/SelfService/LeaveBalance · Access: Guest (linked) · Read-only
Your leave balance rows — one per leave category and year — with entitlement, days used, days pending approval, remaining days, the validity window and the carry-forward value.
Things worth attention
- Balances are maintained by HR; the values refresh when HR runs the balance sync.
- Only your own balance rows are visible.
My Payslips
/SelfService/PayrollDetail · Access: Guest (linked) · Read-only
Your payroll rows generated by HR payroll runs: one row per payroll period with basic salary, income, deduction and take-home pay. The Detail page offers View Slip, which opens the full slip dialog with each income and deduction component and a Print PDF action.
Things worth attention
- Payroll runs that are still in Draft status are excluded from the list, the detail, the export and the slip.
- Only your own payslips are visible; other employees’ slips are never exposed.
My Evaluations
/SelfService/Evaluation · Access: Guest (linked) · Read-only
The performance evaluations recorded for you: one row per evaluation period with the evaluator, final score, rating, evaluation date, status and the recorded note.
Things worth attention
- Evaluations are maintained by HR and managers; this module is read-only.
- Only evaluations recorded for your own employee record are visible.
My Appraisals
/SelfService/Appraisal · Access: Guest (linked) · Read-only
The salary appraisals recorded for you: appraisal type, last rating, current salary, increment percentage, new salary, effective date, status and HR’s note.
Things worth attention
- Appraisals are maintained by HR; this module is read-only.
- Only appraisals recorded for your own employee record are visible.
My Promotions
/SelfService/Promotion · Access: Guest (linked) · Read-only
The promotions recorded for you: the grade you moved from and to, the effective date, the status (Pending, Confirmed or Canceled) and HR’s note.
Things worth attention
- Promotions are maintained by HR; this module is read-only.
- Only promotions recorded for your own employee record are visible.
My Transfers
/SelfService/Transfer · Access: Guest (linked) · Read-only
The transfers recorded for you: the branch, department and designation you moved from and to, the transfer date, the status and HR’s note.
Things worth attention
- Transfers are maintained by HR; this module is read-only.
- Only transfers recorded for your own employee record are visible.
The linking model
- HR creates a confirmed user in Admin → User (a one-time generated password is shown) and then links it on the Employee form’s System User field.
- Saving a link idempotently adds the Guest role; clearing the picker unlinks the account (the role stays — harmless).
- A user can be linked to at most one employee; the rule is enforced in the Employee create/update handlers.
- An unlinked Guest account can still sign in — the Self Service grids then show an empty-state hint asking HR to link the account.
Demo credentials. In demo mode the seeder links the
seeded employee ACME-2001 to a Guest account: sign in with that employee’s e-mail address
(the {first}.{last}@acmeglobal.com address visible on the Employee screen) and the password
123456. The seeded employee has balances, requests, payslips and performance records, so every
Self Service grid is populated. The admin login admin@root.com is linked to the seeded
IT-manager persona ACME-1000 (Alex Morgan), so the admin session also sees fully populated
Self Service grids.
Enterprise Features
Authentication
Full-featured authentication with ASP.NET Core Identity, JWT access tokens with refresh token rotation, and optional Firebase SSO.
| File / Path | Description |
|---|---|
Infrastructures/Authentications/Jwt/JwtService.cs | JWT token generation, refresh token creation, hashing, and validation |
Infrastructures/Authentications/Jwt/JwtAuthEndpoints.cs | Minimal API endpoints: POST /api/auth/* |
Infrastructures/Authentications/Firebase/ | Firebase token verification on server side |
Areas/Identity/Pages/Account/ | Razor Pages for Login, Register, Manage, etc. |
| Config | appsettings.json → JwtSettings |
// 1. Login → POST /api/auth/login with email+password
// 2. Response returns: { token, refreshToken, expiresAt, user }
// 3. When access token expires → POST /api/auth/refresh
// with { refreshToken } → new token pair (rotation)
// 4. Refresh token is hashed (SHA256) and stored in DBSSO Firebase
Indotalent supports Firebase Single Sign-On (SSO) as an optional authentication method.
When enabled, users can sign in using their Google account via Firebase Authentication.
The Firebase configuration is stored in appsettings.json under the SsoFirebase section.
| File / Path | Description |
|---|---|
Infrastructures/Authentications/Firebase/ | Firebase token verification service |
appsettings.json → SsoFirebase | Firebase project configuration |
To enable Firebase SSO, configure the following in appsettings.json:
"SsoFirebase": {
"IsUsed": true,
"ProjectId": "xxx",
"ApiKey": "xxx",
"AuthDomain": "xxx.firebaseapp.com",
"StorageBucket": "xxx.firebasestorage.app",
"MessagingSenderId": "xxx",
"AppId": "xxx"
}
Set "IsUsed": true to enable Firebase SSO. Replace the placeholder values (xxx)
with your actual Firebase project credentials from the Firebase Console.
Set "IsUsed": false to disable Firebase SSO and use only the built-in Identity authentication.
AutoNumber Generation
Entities implementing IHasAutoNumber get auto-generated codes like COMP-0001.
| File / Path | Description |
|---|---|
Data/Interfaces/IHasAutoNumber.cs | Interface definition |
Infrastructures/AutoNumberGenerator/AutoNumberGeneratorService.cs | Number generation service |
| Usage | Add : BaseEntity, IHasAutoNumber to entity |
Background Jobs (Hangfire)
Hangfire with built-in dashboard at /hangfire (Admin only). Supports recurring, fire-and-forget, and delayed jobs.
| File / Path | Description |
|---|---|
Infrastructures/BackgroundJobs/DI.cs | Hangfire configuration + storage |
Infrastructures/BackgroundJobs/HangfireAuthorizationFilter.cs | Admin-only dashboard access |
Infrastructures/BackgroundJobs/Jobs/SerilogCleanupJob.cs | Sample recurring job |
Multi-Database
Switch between InMemory, SQL Server, and PostgreSQL with a single config change. The application supports three database providers - simply toggle "IsUsed" to switch between them.
| File / Path | Description |
|---|---|
Infrastructures/Databases/DatabaseSettingsModel.cs | Configuration model |
Infrastructures/Databases/DI.cs | EF Core provider registration |
appsettings.json | Set "IsUsed": true for your provider |
Configure your database provider in appsettings.json under DatabaseSettings:
"DatabaseSettings": {
// InMemory (default, no external DB needed)
"InMemory": {
"IsUsed": true,
"ConnectionString": "Blazor-HRM",
"TimeoutInSeconds": 1800
},
// Microsoft SQL Server
"MsSQL": {
"IsUsed": false,
"ConnectionString": "Server=localhost\\SQLEXPRESS;Database=MyDb;Trusted_Connection=True;TrustServerCertificate=True",
"TimeoutInSeconds": 1800
},
// PostgreSQL
"PostgreSQL": {
"IsUsed": false,
"ConnectionString": "Host=localhost;Database=MyDb;Username=postgres;Password=yourpassword",
"TimeoutInSeconds": 1800
}
}
To switch providers, set the desired provider's "IsUsed" to true and the others to false.
Only one provider can be active at a time. Update the ConnectionString to match your database server credentials.
Demo Mode
Indotalent includes a Demo Mode feature that, when enabled, automatically seeds the database with dummy demo data on application startup. This is useful for testing, presentations, or evaluation purposes without needing to manually enter data.
| File / Path | Description |
|---|---|
Infrastructures/Databases/DatabaseSeeder.cs | Seeds demo data when Demo Mode is active |
appsettings.json → DemoMode | Toggle Demo Mode on/off |
Configure Demo Mode in appsettings.json:
"DemoMode": {
"IsDemo": true
}
Set "IsDemo": true to enable Demo Mode - the application will seed dummy data
(sample users, roles, and demo records) on every startup.
Set "IsDemo": false to disable it and start with a clean database.
AI Chat
Indotalent includes an AI Chat feature that can be enabled by configuring your preferred AI provider's API key. The application supports multiple AI providers including ChatGPT, Claude, Gemini, and DeepSeek.
| File / Path | Description |
|---|---|
appsettings.json → AiSettings | AI provider selection and API keys |
Configure AI Chat in appsettings.json under AiSettings:
"AiSettings": {
// Choose your provider: "ChatGPT", "Claude", "Gemini", or "DeepSeek"
"Provider": "ChatGPT",
"ChatGPT": {
"ApiKey": "sk-your-chatgpt-api-key",
"Model": "gpt-4o"
},
"Claude": {
"ApiKey": "sk-ant-your-claude-api-key",
"Model": "claude-3-opus-20240229"
},
"Gemini": {
"ApiKey": "your-gemini-api-key",
"Model": "gemini-1.5-pro"
},
"DeepSeek": {
"ApiKey": "your-deepseek-api-key",
"Model": "deepseek-v4-flash"
}
}
To enable AI Chat, set the "Provider" field to your chosen provider name and
fill in the corresponding "ApiKey" with your actual API key from that provider.
Leave the API keys empty to disable the AI Chat feature.
Email Delivery
Multi-provider email service supporting SendGrid, Mailgun, SMTP, and Mailjet. Toggle "IsUsed" to switch between providers.
| File / Path | Description |
|---|---|
Infrastructures/Email/EmailSettingsModel.cs | Provider selection + API keys |
Infrastructures/Email/EmailService.cs | Main email service with templates |
Infrastructures/Email/SendGrid/, Mailgun/, etc. | Provider implementations |
Infrastructures/Email/IdentityEmailSenderAdapter.cs | Identity integration |
Configure email delivery in appsettings.json under EmailSettings:
"EmailSettings": {
// SendGrid
"SendGrid": {
"IsUsed": false,
"ApiKey": "SG.your-sendgrid-api-key",
"FromEmail": "noreply@email.com"
},
// Mailgun
"Mailgun": {
"IsUsed": false,
"ApiKey": "key-your-mailgun-api-key",
"Domain": "mg.yourdomain.com",
"FromEmail": "noreply@email.com"
},
// Mailjet
"Mailjet": {
"IsUsed": false,
"ApiKey": "mj-your-public-key",
"ApiSecret": "mj-your-private-key",
"FromEmail": "noreply@email.com"
},
// SMTP (default)
"Smtp": {
"IsUsed": true,
"Host": "smtp.gmail.com",
"Port": 465,
"UserName": "your-email@gmail.com",
"Password": "your-app-password",
"FromAddress": "your-email@gmail.com",
"FromName": "no-reply"
}
}
To switch email providers, set the desired provider's "IsUsed" to true and the others to false.
Only one provider can be active at a time. Fill in the API keys and credentials for your chosen provider.
File Upload / Download
File storage service supporting local file system with upload, download, delete operations.
| File / Path | Description |
|---|---|
Infrastructures/File/FileStorageService.cs | Core service |
Infrastructures/File/FileStorageSettingsModel.cs | Storage path, allowed extensions, max size |
Infrastructures/File/Local/ | Local file system implementation |
Health Checks
Built-in health check endpoints, hardened for container probes (for example Docker or a load balancer),
plus a System Monitoring dashboard at /Admin/HealthCheck/Index.
| File / Path | Description |
|---|---|
Infrastructures/HealthChecks/DI.cs | Health check registration and endpoint mapping |
Program.cs | Error interceptor bypasses /health* so probes keep raw 200/503 |
appsettings.json - HealthCheckSettings | Toggles for the database and storage checks |
Endpoints
| Endpoint | Purpose | Behavior |
|---|---|---|
/health/live | Liveness (no dependencies) | Always 200 while the process is up |
/healthz | Legacy alias of liveness | Same as /health/live |
/health/ready | Readiness (database + storage) | 200 when Healthy/Degraded, 503 when Unhealthy |
/ready | Legacy alias of readiness | Same as /health/ready |
/health | Full JSON detail for the dashboard | 200/503 with one entry per check |
All probe endpoints are public (no authentication, no rate limiting) so orchestrators can call
them without credentials. The response is never redirected to the error page: an unhealthy readiness check
returns a raw 503, which is exactly what container probes observe.
Degraded vs Unhealthy. Readiness maps Degraded to 200 on purpose:
container probes (for example Docker) only inspect the HTTP status code, so a degraded dependency must not fail a
container healthcheck. Only Unhealthy (for example, the database cannot be reached) produces a
503. Do not expect Degraded to fail a probe - only 503 does.
Docker container probe (example)
A HEALTHCHECK that probes the readiness endpoint - only a 503 response
marks the container unhealthy.
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD curl -f http://localhost:80/health/ready || exit 1curl must exist in the runtime image; orchestrators can alternatively point a
readiness probe at /health/ready.
The dashboard also shows in-memory traffic metrics - see System Monitoring Dashboard (Ops Metrics) below.
System Monitoring Dashboard (Ops Metrics)
The Admin dashboard at /Admin/HealthCheck/Index shows the health cards plus an
Application card, a rolling Traffic summary, and the most recent endpoints.
It is fed by the Admin-only endpoint GET /api/ops/metrics.
| File / Path | Description |
|---|---|
Infrastructures/OpsMetrics/RequestMetricStore.cs | Thread-safe in-memory rolling store (per-minute buckets + bounded latency histogram + recent requests) |
Infrastructures/OpsMetrics/RequestMetricsMiddleware.cs | Records each request; excludes health probes, static assets, and the metrics endpoint itself |
Areas/Admin/HealthCheck/Endpoints/OpsEndpoint.cs | GET /api/ops/metrics minimal API (Admin role) |
Infrastructures/OpsMetrics/DI.cs | AddOpsMetricsService + UseRequestMetricsMiddleware |
appsettings.json - OpsMetricsSettings | Window size, bucket count, recent-request cap |
Configure the rolling window in appsettings.json:
"OpsMetricsSettings": {
"WindowMinutes": 5,
"MaxBuckets": 10,
"MaxRecentRequests": 200
}The snapshot returned by /api/ops/metrics uses camelCase keys:
{
"app": {
"name", "version", "environment", "startedAtUtc", "uptimeSeconds"
},
"window": {
"minutes", "totalRequests", "success", "errors", "errorRate",
"avgMs", "p95Ms", "p99Ms",
"byStatus": { "2xx", "3xx", "4xx", "5xx" }
},
"recent": [
{ "method", "path", "status", "durationMs", "timestampUtc" }
],
"sinceStart": {
"totalRequests", "success", "errors", "errorRate",
"avgMs", "p95Ms", "p99Ms"
}
}successcounts 2xx responses;errorscounts 5xx responses; latency is in milliseconds.- P95/P99 are approximated from a bounded per-minute histogram, so memory stays flat under load.
- Health probes, static assets, and dashboard polls are excluded from the counts.
- Numbers reset whenever the web process restarts - the store is in-memory only.
OpenTelemetry (OTLP Export)
A vendor-neutral observability foundation that exports metrics and traces over
OTLP. It is disabled by default: no exporter runs and no diagnostic listeners start until you set
OpenTelemetrySettings:Enabled to true. The in-memory traffic recording for the System
Monitoring dashboard always runs and is independent of this exporter.
NuGet packages
| Package | Purpose |
|---|---|
OpenTelemetry.Extensions.Hosting | DI integration (services.AddOpenTelemetry()) |
OpenTelemetry.Instrumentation.AspNetCore | Inbound HTTP server metrics and traces |
OpenTelemetry.Instrumentation.Http | Outbound HttpClient metrics and traces |
OpenTelemetry.Exporter.OpenTelemetryProtocol | OTLP exporter (gRPC by default) |
| File / Path | Description |
|---|---|
Infrastructures/OpenTelemetry/ApiMetrics.cs | Static meter Indotalent.Api with counters and a duration histogram |
Infrastructures/OpenTelemetry/DI.cs | AddOpenTelemetryService - only registers providers when enabled |
Infrastructures/OpenTelemetry/OpenTelemetrySettingsModel.cs | Settings model for OpenTelemetrySettings |
Infrastructures/DI.cs | Registration inside AddInfrastructureDI |
appsettings.json - OpenTelemetrySettings | Master switch + OTLP endpoint and headers |
Configure OpenTelemetry in appsettings.json:
"OpenTelemetrySettings": {
"Enabled": false,
"Otlp": {
"Endpoint": "",
"Headers": ""
}
}- Both
Enabled: trueand a non-emptyOtlp:Endpointare required before any exporter starts. - Exports the ASP.NET Core and HttpClient instrumentations plus the app meter
Indotalent.Apiwithindotalent.api.requests,indotalent.api.errors, andindotalent.api.request.duration(taghttp.response.status.family). - The OTLP exporter defaults to the gRPC protocol, so a plain
http://host:4317endpoint works with configuration only. - To use an HTTP/protobuf gateway, some telemetry backends publish an OTLP HTTP gateway endpoint - add
otlp.Protocol = OtlpExportProtocol.HttpProtobuf;inside theAddOtlpExporter(...)call inInfrastructures/OpenTelemetry/DI.cs. - The default
service.nameresource attribute is the assembly name (Indotalent) - use it to identify the application in your telemetry backend.
Logging (Serilog)
Structured logging with Serilog. Writes to rolling files with automatic 3-day cleanup via Hangfire.
| File / Path | Description |
|---|---|
Infrastructures/Logging/Serilog/ | Serilog configuration |
wwwroot/data/serilog/ | Log file output directory |
Infrastructures/BackgroundJobs/Jobs/SerilogCleanupJob.cs | Auto-cleanup job (daily at midnight) |
Rate Limiting
Four rate limiting policies using System.Threading.RateLimiting, configurable via appsettings.json.
| Policy | Scope | Default |
|---|---|---|
| Global | All requests | 100 req / 1 s per IP (fixed window) |
| Authenticated | Authenticated users | 60 req/min per user (sliding window, 6 segments) |
| Write | POST/PUT/DELETE | 20 req/min per user |
| Admin | Admin role | 30 req/min per user |
CORS (Cross-Origin Resource Sharing)
Browsers block cross-origin requests unless the server returns the appropriate
CORS response headers. This project is a monolith - the frontend and
the Web API are served from the same origin - so CORS is not needed today. The setting
exists only to prepare for a future scenario where the Web API (/api/*) is consumed from a
different domain (for example a separate SPA or a partner site). It is disabled by default,
so there is zero behavior change and zero overhead until it is enabled.
| File / Path | Description |
|---|---|
Infrastructures/Cors/CorsSettingsModel.cs | Settings model for CorsSettings |
Infrastructures/Cors/DI.cs | AddCorsService / UseCorsService - both no-ops when disabled |
Infrastructures/DI.cs | AddCorsService registration inside AddInfrastructureDI |
Program.cs | UseCorsService between UseRouting and UseAuthorization |
appsettings.json - CorsSettings | Master switch + origin / method / header allow-list |
| Setting | Default | Description |
|---|---|---|
IsUsed | false | Master switch. When off, no CORS service, middleware, or headers. |
AllowedOrigins | [] | Empty (or "*") allows any origin; a non-empty list acts as an allow-list. |
AllowedMethods | [] | Empty allows any HTTP method. |
AllowedHeaders | [] | Empty allows any request header. |
ExposedHeaders | [] | Response headers the browser may expose to client code; applied only when non-empty. |
AllowCredentials | false | Only honored with an explicit origin allow-list (CORS spec forbids allow-any-origin + credentials). |
Default configuration in appsettings.json:
"CorsSettings": {
"IsUsed": false,
"AllowedOrigins": [],
"AllowedMethods": [],
"AllowedHeaders": [],
"ExposedHeaders": [],
"AllowCredentials": false
}Example - enabling access for one cross-origin single-page app:
"CorsSettings": {
"IsUsed": true,
"AllowedOrigins": [ "https://app.example.com" ],
"AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ],
"AllowedHeaders": [ "Content-Type", "Authorization" ],
"AllowCredentials": true
}IsUsed: falsemeans no CORS service, no CORS middleware, and no CORS headers - the default.- An empty
AllowedOrigins(or one containing"*") allows any origin and cannot be combined with credentials. AllowCredentialsis applied only when an explicit origin allow-list is configured.- The policy is registered globally as the default policy, so it covers
/api/*and the authentication endpoints. - Override per environment in
appsettings.Development.jsonor with the environment variableCorsSettings__IsUsed=true. - After enabling, a request from an allowed origin returns
Access-Control-Allow-Originand the preflightOPTIONSrequest succeeds - verify in the browser DevTools Network tab or withcurl -H "Origin: https://app.example.com" -i.
5. CQRS Pattern (Step-by-Step)
Every feature uses a simple CQRS pattern with plain C# handlers (no MediatR).
Each CRUD operation has its own handler class with a single HandleAsync() method.
Step 1: List Handler
public class GetTaxListHandler
{
private readonly AppDbContext _context;
public GetTaxListHandler(AppDbContext context) => _context = context;
public async Task<ApiResponse<object>> HandleAsync(DataTableRequest request, CancellationToken cancellationToken = default)
{
var query = _context.Tax.AsQueryable();
// Apply search filter
if (!string.IsNullOrWhiteSpace(request.Search))
query = query.Where(x => x.Name.Contains(request.Search) || x.Code.Contains(request.Search));
var projected = query.Select(x => new TaxListItem { ... });
// Shared extension: paging + total + response shape
return await projected.ToDataTableAsync(request, "Tax list retrieved successfully", cancellationToken);
}
}Step 2: Create Handler
public class CreateTaxHandler
{
public async Task<ApiResponse<CreateTaxResponse>> HandleAsync(CreateTaxRequest request, CancellationToken cancellationToken = default)
{
// 1. Validate with FluentValidation
var result = await _validator.ValidateAsync(request, cancellationToken);
if (!result.IsValid)
return ApiResponse<CreateTaxResponse>.Fail(
"Validation failed", result.ToDictionary());
// 2. Check for duplicate Code
if (await _context.Tax.AnyAsync(x => x.Code == request.Code, cancellationToken))
return ApiResponse<CreateTaxResponse>.Fail("Code already exists");
// 3. Save to database
var entity = new Tax
{
Code = request.Code,
Name = request.Name,
PercentageValue = request.PercentageValue,
Description = request.Description
};
_context.Tax.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
return ApiResponse<CreateTaxResponse>.Ok(
new CreateTaxResponse { Id = entity.Id, Code = entity.Code },
"Tax has been created successfully");
}
}Step 3: Update Handler
Similar to Create but loads existing entity, validates it exists, updates properties, and saves - all with the same CancellationToken discipline.
Step 4: Delete Handler
public class DeleteTaxHandler
{
public async Task<ApiResponse<object>> HandleAsync(string id, CancellationToken cancellationToken = default)
{
var entity = await _context.Tax.FindAsync(id, cancellationToken);
if (entity == null)
return ApiResponse<object>.Fail("Tax not found");
// Soft delete only - the global query filter hides the row
_context.SoftDelete(entity);
await _context.SaveChangesAsync(cancellationToken);
return ApiResponse<object>.Ok(new { id }, "Tax deleted successfully");
}
}
The same handlers power both surfaces: Blazor pages resolve them through
HandlerInvoker (fresh DI scope per call), while the Minimal API endpoints
inject them and map the returned ApiResponse<T> to HTTP results.
Standard API Response
All handlers return ApiResponse<T> which wraps the result:
public class ApiResponse<T>
{
public bool Success { get; set; }
public string? Message { get; set; }
public T? Data { get; set; }
public IDictionary<string, string[]>? Errors { get; set; }
}6. Minimal API Endpoints
The HTTP API surface uses ASP.NET Core Minimal API. Blazor pages call the same CQRS handlers
in-process through HandlerInvoker; the endpoints remain for HTTP clients, file
downloads, and the OpenAPI/Swagger surface. Each feature registers its endpoints
in a single {EntityName}Endpoint.cs file, on ONE feature-scoped group
(/api/{entityLower}).
| Method | Route | Action | Auth |
|---|---|---|---|
| GET | /api/{entity} | Paginated list with search & sort | Required |
| GET | /api/{entity}/{id} | Get by ID | Required |
| GET | /api/{entity}/export | Full Excel export (same search filter) | Required |
| GET | /api/{entity}/{lookup}-lookup | Lookup list for autocomplete | Required |
| GET | /api/{entity}/{id}/items | Master-detail items (master-detail features) | Required |
| POST | /api/{entity} | Create new record | Required |
| PUT | /api/{entity} | Update existing record | Required |
| DELETE | /api/{entity}/{id} | Soft-delete record | Required |
Endpoints are registered in the area mapping (Areas/{Area}/Map{Area}Endpoints.cs) via app.Map{EntityName}Endpoints(); - never directly in Program.cs.
When the API is consumed from a different origin, enable CORS via CorsSettings - see the CORS section above.
7. Blazor Components Tutorial
The frontend is a Blazor Web App using interactive server render mode + MudBlazor.
Every page is a Razor component (Pages/*.razor) rendered on the server; UI events travel
over the SignalR circuit. There is no SPA routing, no client-side build tooling, and no per-page
JavaScript - interactions are C# event handlers, and data access goes through CQRS handlers.
How the UI is Hosted
App.razor -> document shell + global assets (MudBlazor, SheetJS, jsPDF, ECharts, interop helpers)
Routes.razor -> router (interactive server render mode, MainLayout)
Layouts/ -> MainLayout.razor (app bar, brand, user menu, Module Information drawer) + WorkspaceTopNav.razor
Services/ -> HandlerInvoker, PageState, ModuleInfoState, CircuitUserState
Shared/ -> reusable components (Crud* pages, AuditTrailCard, LookupAutocomplete, chips, ...)
MudBlazor providers (theme, dialog, snackbar, popover) live in MainLayout.razor, so every
page rendered inside the layout can use dialogs and snackbars without extra setup.
Basic Page Component Pattern
Every feature page follows this pattern:
@* Routable page: keeps the legacy /Area/Entity/Index URL *@
@page "/Main/Todo/Index"
@page "/Main/Todo"
@layout MainLayout
@attribute [Authorize(Roles = $"{ApplicationRoles.AdminConst},{ApplicationRoles.MemberConst}")]
@inject HandlerInvoker Invoker
@inject PageState PageState
<PageTitle>Todo Management - @Configuration["AppSettings:Name"]</PageTitle>
@if (!_ready)
{
<div class="ui-empty" style="padding-top:4rem;">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
<p class="u-text-muted">Loading...</p>
</div>
}
else
{
@* page content *@
}
@code {
private bool _ready;
protected override void OnInitialized() => PageState.Set("Todo Management");
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || _ready) return;
await Task.Delay(500); // page gate (Section 3.15.1)
await LoadAsync();
_ready = true;
StateHasChanged();
}
}
Data access always goes through HandlerInvoker.InvokeAsync(sp => sp.GetRequiredService<GetTodoListHandler>().HandleAsync(...)) -
never a direct handler instantiation and never a fetch call.
Example 1: Index Page (Search + Paged Grid + Export)
Pure Master and Lookup features use the declarative CrudIndex component - the page only wires descriptors and handler lambdas (see Areas/Admin/Country/Pages/Index.razor).
<CrudIndex TItem="CountryListItem"
Title="Country List"
EntityLabel="country"
SearchPlaceholder="Search by Code, Name, or Description..."
EmptyText="No country records found."
CreateUrl="/Admin/Country/Create"
DetailUrlFormat="/Admin/Country/Detail/{0}"
EditUrlFormat="/Admin/Country/Edit/{0}"
Columns="_columns"
Loader="LoadAsync"
Exporter="ExportAsync"
ExportFileNamePrefix="country"
ExportSheetName="Countries"
IdSelector="@(item => item.Id)"
DefaultOrderColumn="2">
<ModuleInfoContent>
<CountryModuleInfo />
</ModuleInfoContent>
</CrudIndex>
@code {
private static readonly FieldDescriptor[] _columns =
{
new() { Name = nameof(CountryListItem.AutoNumber), Label = "Auto Number", ShowInIndex = true, SortOrder = 1, Format = CellFormat.Dash },
new() { Name = nameof(CountryListItem.Code), Label = "Code", ShowInIndex = true, SortOrder = 2 },
new() { Name = nameof(CountryListItem.Name), Label = "Name", ShowInIndex = true, SortOrder = 3 },
new() { Name = nameof(CountryListItem.Description), Label = "Description", ShowInIndex = true, SortOrder = 4, Format = CellFormat.Dash }
};
private async Task<DataTableResponse<CountryListItem>?> LoadAsync(DataTableRequest request, CancellationToken cancellationToken)
{
var result = await Invoker.InvokeAsync(sp =>
sp.GetRequiredService<GetCountryListHandler>().HandleAsync(request, cancellationToken));
return result.Success && result.Data is DataTableResponse<CountryListItem> data ? data : null;
}
}private async Task<GridData<TodoListItem>> LoadServerDataAsync(GridState<TodoListItem> state, CancellationToken cancellationToken)
{
var sort = state.SortDefinitions.FirstOrDefault();
var request = new DataTableRequest
{
Search = _search,
Page = state.Page + 1,
PageSize = state.PageSize,
Start = 0, // zero so ResolvePaging() cannot overwrite Page/PageSize
Length = 0,
OrderColumn = MapSortColumn(sort?.SortBy) ?? 1,
OrderDir = sort is null ? "asc" : sort.Descending ? "desc" : "asc"
};
var result = await Invoker.InvokeAsync(sp =>
sp.GetRequiredService<GetTodoListHandler>().HandleAsync(request, cancellationToken));
return result.Success && result.Data is DataTableResponse<TodoListItem> data
? new GridData<TodoListItem> { Items = data.Items, TotalItems = data.Total }
: new GridData<TodoListItem> { Items = Array.Empty<TodoListItem>(), TotalItems = 0 };
}
The search box is debounced in the page (300ms + cancelled request), sorting maps the grid column to the raw
DataTableRequest.OrderColumn index, and the Export XLSX button builds friendly-header rows and calls
blazorExport.xlsx(fileName, sheetName, rows) from the shared interop file.
Example 2: Create Form with Validation
Bespoke features use a {Entity}Form component (see Areas/Main/Todo/Components/TodoForm.razor); pure/lookup features get this for free from CrudEditPage + CrudForm.
// Defaults live in property initializers - no post-render patching
public class TodoEditModel
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public TodoPriority? Priority { get; set; }
public DateTime? DueDate { get; set; }
public TimeSpan? DueTime { get; set; }
public decimal Progress { get; set; }
public string? OwnerUserId { get; set; }
public bool IsCompleted { get; set; }
public List<TodoItemModel> Items { get; set; } = new();
}
// Page fields: _model, _ready, _saving, _created, _createdId, _generalError, _errorspublic bool Validate()
{
ClearErrors();
if (string.IsNullOrWhiteSpace(Model.Name))
_errors["Name"] = "Todo Name is required";
else if (Model.Name.Length > 100)
_errors["Name"] = "Todo Name must not exceed 100 characters";
if (Model.Description?.Length > 500)
_errors["Description"] = "Description must not exceed 500 characters";
if (Model.Progress < 0 || Model.Progress > 100)
_errors["Progress"] = "Progress must be between 0 and 100";
StateHasChanged();
return _errors.Count == 0;
}
public void SetServerErrors(string? message, IDictionary<string, string[]>? errors)
{
ClearErrors();
if (errors is not null)
foreach (var (key, messages) in errors)
_errors[key] = messages.FirstOrDefault() ?? string.Empty;
_generalError = message;
StateHasChanged();
}private async Task SubmitAsync()
{
if (_form is null || !_form.Validate()) return;
_saving = true;
_form.ClearErrors();
StateHasChanged();
try
{
await Task.Delay(2000); // Simulated processing delay
var request = _model.ToCreateRequest();
var result = await Invoker.InvokeAsync(sp =>
sp.GetRequiredService<CreateTodoHandler>().HandleAsync(request));
if (result.Success && result.Data is not null)
{
_created = true;
_createdId = result.Data.Id;
Snackbar.Add($"Todo \"{_model.Name}\" has been created successfully.", Severity.Success);
}
else
{
_form.SetServerErrors(result.Message, result.Errors);
Snackbar.Add(result.Message ?? "Failed to create todo", Severity.Error);
}
}
catch
{
Snackbar.Add("An error occurred while submitting the form.", Severity.Error);
}
finally
{
_saving = false;
StateHasChanged();
}
}Example 3: Loading States Pattern
Every page declares the same UI state fields for a polished UX:
// Page gate + spinner states (bound to Disabled / spinner visibility)
private bool _ready; // controls the @if (!_ready) loading gate
private bool _saving; // "Save" -> "Saving..." + spinner
private bool _exporting; // "Export XLSX" -> "Exporting..." + spinner
private bool _deleting; // "Delete" -> "Deleting..." + spinner
private string _error = string.Empty; // inline error banner, auto-cleared after 4s
// Delays: page gate 500ms; submit + dialogs 2000ms; export + refresh 500msExample 4: Standard Delete Confirmation Dialog
Delete operations use the shared _DeleteConfirmationDialog component (never confirm()):
private async Task ConfirmDeleteAsync()
{
if (_data is null) return;
var confirmed = await _DeleteConfirmationDialog.ShowAsync(
DialogService,
"Delete Todo",
"Are you sure you want to delete this todo record?");
if (!confirmed) return;
_deleting = true;
StateHasChanged();
try
{
var result = await Invoker.InvokeAsync(sp =>
sp.GetRequiredService<DeleteTodoHandler>().HandleAsync(Id));
if (result.Success)
{
_deleted = true;
Snackbar.Add($"Todo \"{_data.Name}\" has been deleted successfully.", Severity.Success);
await Task.Delay(800);
Navigation.NavigateTo("/Main/Todo/Index");
}
else
{
Snackbar.Add(result.Message ?? "Failed to delete", Severity.Error);
}
}
finally
{
_deleting = false;
StateHasChanged();
}
}Blazor Page Checklist
When creating a new Blazor page, ensure you include:
@page + @layout MainLayout + @attribute [Authorize(Roles = ...)]_ready gate + PageState.Set(...) in OnInitializedHandlerInvoker - never new, never fetchOnAfterRenderAsync (charts, export, PDF)ui-* CSS classes; no per-page <script>/<style>Disabled + spinner + text change)_DeleteConfirmationDialog (not confirm())Snackbar; error banners auto-clear after 4s8. AI-Assisted Development
Indotalent ships with an automatic AI-assisted development pipeline driven by the
.ai-assisted/ folder. The only file the developer writes is
.ai-assisted/DATA-DICTIONARY.md - the AI generates everything else: the feature
specification, the technical PRD, and the complete application.
How to Start the Development Sequence (automatic)
- Fill
.ai-assisted/DATA-DICTIONARY.md- application name, persona, and feature description. - Start the sequence - tell your AI coding agent exactly this command:
start coding - The AI runs the whole chain automatically: Gate 0 (identity check) → DATA-DICTIONARY review → Phase 0 (
FEATURE.md) → Phase 1 (PRD.md) → Phase 2 (build the application). - Done! A ready-to-use application, verified with
dotnet build(0 errors) after every feature.
Important - before you start: make sure .ai-assisted/DATA-DICTIONARY.md
has been updated to match the new application you are about to build. The AI builds
exactly what that file describes - template placeholders ([ ... ]), the
## EXAMPLE app, or data from a previous project would be built as-is.
What the AI Generates Automatically
One command produces three deliverables:
FEATURE.md - business source of truth (Phase 0)PRD.md - technical blueprint / build backlog (Phase 1)Entity Types Auto-Detected by AI
| Pattern in Entity | Detected Type |
|---|---|
public ICollection<T>? Items { get; set; } | Master-Detail |
public string {X}Id { get; set; } + navigation property | With Lookup |
| Neither pattern above | Pure Master Data |
: BaseEntity, IHasAutoNumber | Adds auto-numbering |
Each Feature Is Generated With 16+ Files (Blazor vertical slice)
For every feature, the AI creates the full vertical slice - no controllers, no per-page JavaScript:
Maintenance Mode - Adding a Single Feature
Once the application is customized (AppSettings:Name is no longer
Indotalent), the pipeline is inactive. To add a single feature, work directly with
.ai-assisted/SKILL-SOFTWARE-ENGINEERING.md: create the entity class in
Data/Entities/{Entity}.cs and let the AI generate the feature following the skill.
Prompt Examples - Copy & Use (maintenance mode)
These per-feature prompts apply when the pipeline is inactive (maintenance mode). For a greenfield
project, use the single command start coding instead. Replace {Entity}
with your entity name.
Generate a simple CRUD feature with no relationships:
Generate full CRUD for {Entity}. Follow the skill.Generate a feature that references another entity via foreign key:
Generate full CRUD for {Entity} with lookup to {LookupEntity}. Follow the skill.Generate a header-detail feature (e.g., Sales Order with line items):
Generate full CRUD for {MasterEntity} with {DetailEntity}. Follow the skill.Generate a feature with pre-populated seed data:
Generate full CRUD for {Entity} with seed data. Follow the skill.Smart Prompt Strategies
To get the best results from your AI agent and save tokens, use these strategies:
"Read Areas/Admin/Currency/ and generate a new feature following the same pattern."
This restricts the AI to just the Currency feature folder, saving thousands of tokens.
"Generate full CRUD for Category. Use Tax as the template. Follow the skill."
The AI will use Tax as a reference and adapt it for Category.
"Make me a CRUD" → Too vague. The AI doesn't know your patterns.
"Generate full CRUD for Category. Follow the skill." → The AI knows exactly what to do.
"Generate full CRUD for Category, Product, and Customer. Follow the skill."
One prompt, multiple entities. The AI processes each independently.
For the complete set of ready-to-use prompts (Options 1–5), open
.ai-assisted/SKILL-SOFTWARE-ENGINEERING.md → section
"For Users: What to Say to Your AI".
Blazor HRM - Technical Documentation v1.0
Built with ASP.NET Core Blazor Server 10 · MudBlazor · Hangfire · Serilog · EF Core · VSA Architecture