Indotalent Enterprise Kit Documentation
A comprehensive guide to the architecture, features, and development workflow of the Indotalent ASP.NET Core Blazor Server enterprise starter kit.
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 |
| Branch Scope | IHasBranch + BranchId with per-entity FK/index (32 entities) |
| 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, Currency, etc.) | Admin role |
Areas/SuperAdmin/ | Super-admin system features (Tenant, Tenant User, User, Health Check, Serilog, System File, Job Dashboard) | SuperAdmin role |
Areas/Main/ | Main features (CRM Dashboard, Sales Team, Sales Representative, Campaign, Budget, Expense, Lead, Lead Contact, Lead Activity, Todo, Todo Dashboard) | Member / Main role |
Areas/Sales/ | Sales features (Sales Quotation, Sales Order, Delivery Order, Sales Return, Credit Note, Invoice, Payment Receive) | Sales role |
Areas/Purchase/ | Purchase features (Purchase Requisition, Purchase Order, Goods Receive, Purchase Return, Debit Note, Bill, Payment Disburse) | Purchase role |
Areas/Inventory/ | Inventory features (Unit Measure, Product, Warehouse, Transfers, Adjustments, Stock Counts, Reports) | Inventory role |
Areas/ThirdParty/ | Third party features (Customer, Vendor, Contacts, Categories, Groups) | ThirdParty role |
Areas/Utilities/ | Utility features (Bookings, Program Managers, Payment Methods) | Utilities role |
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 logo, footer, and app-bar brand -
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: workspace top navigation, app-bar brand, Module Information drawer |
appsettings.json → AppSettings | Central application name configuration |
Configure the application name in appsettings.json under AppSettings:
"AppSettings": {
"Name": "Indotalent"
}
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 68 functional modules organized into nine areas. 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 | 11 | CRM workspace: dashboards, sales teams, campaigns, leads, todo |
| Third Party | Admin, ThirdParty | 8 | Customer and vendor master data with contact persons |
| Sales | Admin, Sales | 7 | Quotation-to-collection sales cycle |
| Purchase | Admin, Purchase | 7 | Requisition-to-payment procurement cycle |
| Inventory | Admin, Inventory | 13 | Product master data, stock movements and inventory reports |
| Utilities | Admin, Utilities | 6 | Booking, program management and payment method lookups |
| AI | Admin, AI | 1 | Conversational assistant over the configured LLM provider |
| Admin | Admin | 8 | Identity, reference data, sequences, company profile and the branch master |
| Super Admin | SuperAdmin | 7 | Tenant workspaces, tenant users, system users, monitoring, logs, deployment files and jobs |
| Total | 68 |
Access is enforced per page with [Authorize(Roles = ...)]. The Admin role can open every
business area; each area then additionally admits its module role (for example Sales needs the Sales role, Main
accepts Member, Main or Admin). The Super Admin area is the exception:
it requires the SuperAdmin role specifically and its screens are exempt from tenant selection. 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; some modules add more, such as Roles, Password or Download).
- The header offers Create New and Export XLSX; the export honors the active search filter and resolves audit fields to e-mail addresses.
- Transactional documents use the header-then-lines pattern: create the header first, then add lines on the Edit page. Line changes are saved immediately through their own handlers, so stored Sub Total, Tax Amount and TOTAL are always current.
- Documents can be printed to PDF from the Detail page; lists export to XLSX instead.
- Deletes are soft deletes; where noted, deleting a master record also soft-deletes its child rows.
- Auto-numbered records receive a stable reference from the shared sequence engine - see AutoNumber Generation.
Catalogue type legend
The Type column in each area catalogue classifies the screen by its data shape: Master Data (standalone reference records), With Lookup (records that reference another module), Master-Detail (a document header with line items or child rows), Dashboard (read-only aggregated metrics) and Report (read-only query view with export). A few Admin screens are labeled Identity, Configuration or Utility where those fit better than the generic types.
Demo data included. All modules ship with seeded demo records (customers, vendors, products, full sales and purchase cycles, leads, todos) so every screen is populated on first run. In Demo Mode the destructive operations are disabled and sensitive values stay masked.
Module index
Jump directly to any module card:
Main: CRM Dashboard · Sales Team · Sales Representative · Campaign · Budget · Expense · Lead · Lead Contact · Lead Activity · Todo · Todo Dashboard
Third Party: Customer · Customer Category · Customer Group · Customer Contact · Vendor · Vendor Category · Vendor Group · Vendor Contact
Sales: Sales Quotation · Sales Order · Delivery Order · Sales Return · Credit Note · Invoice · Payment Receive
Purchase: Purchase Requisition · Purchase Order · Goods Receive · Purchase Return · Debit Note · Bill · Payment Disburse
Inventory: Unit Measure · Product Group · Product · Warehouse · Transfer Out · Transfer In · Positive Adjustment · Negative Adjustment · Scrapping · Stock Count · Stock Report · Movement Report · Transaction Report
Utilities: Booking Group · Booking Resource · Booking · Program Manager · Program Resource · Payment Method
AI: AI Chat
Admin: User · Role · Tax · Country · Currency · Auto Number Sequence · Company · Branch
Super Admin: Tenant · Tenant User · System User · Health Check · Serilog · System File · Job Dashboard
Main
The Main area is the CRM workspace: analytics dashboards, sales team organization, campaign planning with budgets and expenses, the lead pipeline with contacts and activities, and personal task management. It is the default landing area after sign-in. Access requires Admin, Member or Main role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| CRM Dashboard | /Main/CrmDashboard | Dashboard | Live metrics across leads, campaigns, orders, invoices and budgets |
| Sales Team | /Main/SalesTeam | Master Data | Registry of sales teams with an inline members editor |
| Sales Representative | /Main/SalesRepresentative | With Lookup | Salespeople with their contact data and team assignment |
| Campaign | /Main/Campaign | Master-Detail | Marketing campaigns with inline budgets, expenses and leads panels |
| Budget | /Main/Budget | With Lookup | Campaign budget planning records |
| Expense | /Main/Expense | With Lookup | Campaign spending records |
| Lead | /Main/Lead | Master-Detail | Sales lead with company profile, BANT scores and pipeline stage |
| Lead Contact | /Main/LeadContact | With Lookup | Contact persons attached to a lead |
| Lead Activity | /Main/LeadActivity | With Lookup | Logged interactions (calls, meetings, e-mails) for a lead |
| Todo | /Main/Todo | Master-Detail | Tasks with checklist items, attachments, priority and progress |
| Todo Dashboard | /Main/TodoDashboard | Dashboard | Read-only analytics over the todo dataset |
CRM Dashboard
/Main/CrmDashboard · Access: Admin, Member, Main · Dashboard
Aggregates live metrics across leads, campaigns, sales orders, purchase orders, invoices and budgets. All numbers are computed from the same data the module screens maintain, so the dashboard never needs separate bookkeeping.
What you can do
- Track pipeline value, win rate, monthly sales, pending payments and campaign spend at a glance.
- Compare sales versus purchase per month and per week (amounts in thousands).
- Review deal stages, lead sources, conversion metrics, the sales leaderboard and campaign ROI.
Things worth attention
- Pipeline values come from Lead targeted amounts; win statistics use the Closed Won closing status.
- Pending payments sum confirmed invoices that are not fully paid.
- Use Refresh to recompute every metric and redraw the charts.
Sales Team
/Main/SalesTeam · Access: Admin, Member, Main · Master Data
The CRM registry of sales teams. Each team stores a name and description plus any number of members maintained inline through the members editor; member rows are stored as Sales Representative records.
What you can do
- Browse, search and page through sales teams (server-side grid).
- Use Create New to register a team and Export XLSX to download the filtered list.
- Maintain team members directly from the team form through the inline members editor.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters; duplicated names are rejected. |
| Members | Sales representatives edited inline and stored as Sales Representative rows. |
Things worth attention
- Search matches Auto Number, Name or Description.
- Deleting a team is a soft delete that also soft-deletes its member rows.
- Campaigns and Leads reference these records through the Sales Team lookup.
Sales Representative
/Main/SalesRepresentative · Access: Admin, Member, Main · With Lookup
A CRUD screen for sales representative records: the individual salespeople with their job title, employee number and contact details, each assigned to a sales team.
What you can do
- Browse, search and page through sales representative records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Job Title / Employee Number | Optional identification fields. |
| Phone Number / Email Address | Optional contact fields. |
| Sales Team | Lookup to the Sales Team module. |
Things worth attention
- Search matches Auto Number, Name, Job Title, Employee Number, Phone Number, Email Address and Description.
- Members are also maintained inline from the Sales Team module through its members editor.
Campaign
/Main/Campaign · Access: Admin, Member, Main · Master-Detail
Manages marketing campaigns with their planning panels: budgets, expenses and leads are edited inline on the campaign Edit page and stored in their own modules, so campaign planning and the standalone Budget, Expense and Lead screens always show the same rows.
What you can do
- Browse, search and page through campaigns (search matches Auto Number, Title, Description, Sales Team or Status).
- Use Create New to register a campaign and Export XLSX to download the filtered list.
- On the Edit page use the embedded Budgets, Expenses and Leads panels to add or change planning rows.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Campaign being prepared. |
| Cancelled | Voided campaign, kept for history. |
| Confirmed | Approved and ready to run. |
| OnProgress | Campaign is running. |
| OnHold | Temporarily paused. |
| Finished | Campaign completed. |
| Archived | Closed campaign retained for reference. |
Things worth attention
- Panel rows are saved immediately through the Budget, Expense and Lead modules, so they also appear in those modules.
- Deleting a campaign soft-deletes its budget, expense and lead rows.
- Campaign ROI on the CRM dashboard is computed from campaign budgets and expenses.
Budget
/Main/Budget · Access: Admin, Member, Main · With Lookup
A CRUD screen for campaign budget records: a title, budget date, amount and status, each tied to a campaign. Budgets are campaign planning records and are also surfaced inside the Campaign screen.
What you can do
- Browse, search and page through budget records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Planned budget not yet approved. |
| Cancelled | Voided budget line, kept for history. |
| Confirmed | Approved amount counted in campaign spend. |
| Archived | Closed record retained for reference. |
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Title / Budget Date / Amount | Required title; date and numeric amount of the budget. |
| Campaign | Lookup to the Campaign module. |
Things worth attention
- Search matches Auto Number, Title, Status and Description.
- Budgets are edited from both the Budget module and the Campaign planning panel.
Expense
/Main/Expense · Access: Admin, Member, Main · With Lookup
A CRUD screen for campaign expense records: a title, expense date, amount and status, each tied to a campaign. Expenses are campaign spending records and are also surfaced inside the Campaign screen.
What you can do
- Browse, search and page through expense records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Planned expense not yet approved. |
| Cancelled | Voided expense, kept for history. |
| Confirmed | Approved spend counted in campaign ROI. |
| Archived | Closed record retained for reference. |
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Title / Expense Date / Amount | Required title; date and numeric amount of the expense. |
| Campaign | Lookup to the Campaign module. |
Things worth attention
- Search matches Auto Number, Title, Status and Description.
- Expenses are edited from both the Expense module and the Campaign planning panel.
Lead
/Main/Lead · Access: Admin, Member, Main · Master-Detail
The core CRM lead record: a potential deal with its company profile, target and closed amounts, close dates, BANT qualification scores and pipeline stage. Contacts and activities are maintained inline from the lead, and the pipeline stage and closing status drive the CRM dashboard metrics.
What you can do
- Browse, search and page through lead records (server-side grid).
- Use Create New to add a lead and Export XLSX to download the currently filtered list.
- Maintain lead contacts and lead activities from the embedded editors on the lead form.
Pipeline stage
| Stage | Meaning |
|---|---|
| 1. Prospecting | Initial research and outreach. |
| 2. Qualification | Fit and budget are being qualified. |
| 3. NeedAnalysis | Customer needs are being analyzed. |
| 4. Proposal | A proposal has been presented. |
| 5. Negotiation | Terms and pricing are being negotiated. |
| 6. DecisionMaking | Awaiting the customer decision. |
| 7. Closed | Deal concluded; the closing status records the outcome. |
Closing status is one of ClosedLost, ClosedWon or OnProgress; win statistics on the CRM dashboard use the Closed Won status.
Key properties
| Property | Description |
|---|---|
| Title | Required, up to 100 characters. |
| Campaign / Sales Team | Optional lookups to the Campaign and Sales Team modules. |
| Company profile | Name, description, address (street, city, state, zip, country), phone, fax, e-mail, website and socials (WhatsApp, LinkedIn, Facebook, Instagram, Twitter). |
| Dates | Prospecting date, closing estimation and actual closing date. |
| Amounts | Amount targeted and amount closed; targeted amounts feed the pipeline value metric. |
| BANT scores | Budget, Authority, Need and Timeline scores, each a percentage between 0 and 100. |
| Closing Note | Optional text describing the outcome. |
Things worth attention
- Search covers the title, description, the whole company profile, pipeline stage, closing status and closing note.
- Pipeline stage and closing status drive the dashboard's deal stages and win-rate metrics.
- Contacts and activities are stored in their own modules and also surface there.
Lead Contact
/Main/LeadContact · Access: Admin, Member, Main · With Lookup
A CRUD screen for the people you deal with on a lead: full name, address, contact details and social profiles, each linked to a lead record.
What you can do
- Browse, search and page through lead contact records (server-side grid).
- Use Create New to add a contact and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Lead | Reference to the owning Lead record. |
| Full Name | Required, up to 100 characters. |
| Contact details | Address fields, phone, fax, mobile, e-mail and website. |
| Social profiles | WhatsApp, LinkedIn, Facebook, Twitter and Instagram. |
| Avatar | Avatar upload is not exposed in the UI; initials are shown instead. |
Things worth attention
- Search matches Auto Number, Full Name, Description, the address and contact fields, and the social profiles.
- Contacts are also maintained inline from the Lead module through its contacts editor.
Lead Activity
/Main/LeadActivity · Access: Admin, Member, Main · With Lookup
A CRUD screen for logged interactions with a lead: calls, e-mails, meetings, events, social media contact and other touchpoints, each with a summary, time window and optional attachment.
What you can do
- Browse, search and page through lead activity records (server-side grid).
- Use Create New to log an activity and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Lead | Reference to the owning Lead record. |
| Summary | Required, up to 100 characters. |
| Type | Other, Phone, Email, Social Media, Meeting or Event. |
| From / To Date | Time window of the interaction. |
| Attachment Name / Description | Optional attachment reference and free-text notes. |
Things worth attention
- Search matches Auto Number, Summary, Type, Attachment Name and Description.
- Activities are also maintained inline from the Lead module through its activities editor.
Todo
/Main/Todo · Access: Admin, Member, Main · Master-Detail
A task-management module for personal or team to-dos. Each to-do captures a title, optional description, priority, category, a schedule (due date and time), numeric progress, a completion state and an optional owner. A to-do is a master record that can hold multiple child checklist items as well as image and file attachments.
What you can do
- Browse, search, sort and page through all to-dos (server-side grid).
- Use Create New to add a to-do and Export XLSX to download the currently filtered list.
- Manage checklist items and attachments on the Create, Edit and Detail screens.
Key properties
| Property | Description |
|---|---|
| Name / Description | Required task title (up to 200 characters) and optional longer details. |
| Priority | Optional Low, Medium or High, rendered as color chips. |
| Category | Optional Personal, Work or Learning, each with its own icon. |
| Due Date / Time | Optional target schedule; the grid displays the date. |
| Progress | Numeric 0-100 with decimals; the bar is red below 30, amber from 30 to 69 and green from 70 up. |
| Status | Derived solely from the completed flag: Completed or Pending. |
| Owner | Optional responsible user; the grid shows the owner's e-mail address. |
| Items | Child checklist lines (each with assignee, start date/time and completion state), shown as a badge. |
| Audit | Created At/By and Updated At/By tracking; records support soft delete. |
Things worth attention
- Global search matches name, description, tags, priority, category, auto number and owner e-mail.
- Status is independent of Progress - update both intentionally.
- Attachments: a to-do can carry checklist items plus image and file attachments.
Todo Dashboard
/Main/TodoDashboard · Access: Admin, Member, Main · Dashboard
A read-only analytics view over the whole to-do dataset. It loads a single payload (summary, priority breakdown, category breakdown, six-month flow and the ten latest to-dos) and renders it as KPI widgets, bar and donut charts, and a recent-records table.
Screen elements
- Four KPI widgets: Total Tasks, Completed, Pending and Overdue, each with a supporting subtitle.
- Tasks by Priority: grouped bars comparing Completed with Pending for Low, Medium, High and Unassigned buckets.
- Completion Status and Category Share donuts, with the completion rate or total count in the center.
- Monthly Task Flow: records created versus records due for the current month and the five preceding months.
- Recent Todos: the ten newest to-dos with priority, category, due date, progress, status, owner and a Detail link.
- Refresh re-fetches the payload without reloading the page.
Things worth attention
- All widgets, charts and the table are built from the same payload, so they always agree on totals.
- Overdue and due-today are evaluated against the server clock in UTC, so day boundaries may differ from local time.
- Soft-deleted records are excluded by the global query filter and never appear in any metric.
- The dashboard is read-only; create, edit, complete and delete actions live in the Todo module.
Third Party
The Third Party area holds the counterparty master data used across sales and purchasing: customers and vendors with their profiles and contact persons, plus the category and group lookups that classify them. Sales documents reference customers; purchase documents reference vendors. Access requires the Admin or ThirdParty role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Customer | /ThirdParty/Customer | With Lookup | Customer registry with profile and inline contact persons |
| Customer Category | /ThirdParty/CustomerCategory | Master Data | Category lookup referenced by customers |
| Customer Group | /ThirdParty/CustomerGroup | Master Data | Group lookup referenced by customers |
| Customer Contact | /ThirdParty/CustomerContact | With Lookup | Contact persons attached to a customer |
| Vendor | /ThirdParty/Vendor | With Lookup | Vendor registry with profile and inline contact persons |
| Vendor Category | /ThirdParty/VendorCategory | Master Data | Category lookup referenced by vendors |
| Vendor Group | /ThirdParty/VendorGroup | Master Data | Group lookup referenced by vendors |
| Vendor Contact | /ThirdParty/VendorContact | With Lookup | Contact persons attached to a vendor |
Customer
/ThirdParty/Customer · Access: Admin, ThirdParty · With Lookup
The CRM registry of customers. Each customer stores a profile (address, contact details, social media) plus any number of contact persons maintained inline through the contacts editor and stored as Customer Contact rows.
What you can do
- Browse, search and page through customers (server-side grid).
- Use Create New to register a customer and Export XLSX to download the currently filtered list.
- Maintain contact persons directly in the customer form through the inline contacts editor.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters; duplicated names are rejected. |
| Profile | Address fields, country, phone, fax, e-mail, website and socials (WhatsApp, LinkedIn, Facebook, Instagram, Twitter/X, TikTok). |
| Customer Group / Category | Optional lookups backed by the Customer Group and Customer Category modules. |
| Contacts | Contact persons edited inline and stored as Customer Contact rows. |
Things worth attention
- Search matches Auto Number, Name, City, Phone, E-mail and the group and category names.
- Deleting a customer is a soft delete that also soft-deletes its contact rows.
- Sales documents reference these records through the Customer lookup.
Customer Category
/ThirdParty/CustomerCategory · Access: Admin, ThirdParty · Master Data
A CRUD screen for customer category records used as a classification lookup. Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through customer category records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- The Customer module references these records through the Customer Category lookup.
Customer Group
/ThirdParty/CustomerGroup · Access: Admin, ThirdParty · Master Data
A CRUD screen for customer group lookup records used to categorize customers. Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through customer group records (server-side grid).
- Use Create New to add a group and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters; duplicated names are rejected. |
| Description | Optional; the grid shows an em dash when empty. |
Things worth attention
- Search matches Auto Number, Name and Description.
- The Customer module references these records through the Customer Group lookup.
Customer Contact
/ThirdParty/CustomerContact · Access: Admin, ThirdParty · With Lookup
A CRUD screen for customer contact persons: name, job title, phone and e-mail, each linked to a customer record.
What you can do
- Browse, search and page through customer contact records (server-side grid).
- Use Create New to add a contact and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Customer | Reference to the owning Customer record. |
| Name | Required, up to 100 characters. |
| Job Title / Phone / E-mail | Optional contact identification fields. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name, Job Title, Phone Number, Email Address and Description.
- Contacts are also maintained inline from the Customer module through its contacts editor.
Vendor
/ThirdParty/Vendor · Access: Admin, ThirdParty · With Lookup
The CRM registry of vendors. Each vendor stores a profile (address, contact details, social media) plus any number of contact persons maintained inline through the contacts editor and stored as Vendor Contact rows.
What you can do
- Browse, search and page through vendors (server-side grid).
- Use Create New to register a vendor and Export XLSX to download the currently filtered list.
- Maintain contact persons directly in the vendor form through the inline contacts editor.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters; duplicated names are rejected. |
| Profile | Address fields, country, phone, fax, e-mail, website and socials (WhatsApp, LinkedIn, Facebook, Instagram, Twitter/X, TikTok). |
| Vendor Group / Category | Optional lookups backed by the Vendor Group and Vendor Category modules. |
| Contacts | Contact persons edited inline and stored as Vendor Contact rows. |
Things worth attention
- Search matches Auto Number, Name, City, Phone, E-mail and the group and category names.
- Deleting a vendor is a soft delete that also soft-deletes its contact rows.
- Purchase documents reference these records through the Vendor lookup.
Vendor Category
/ThirdParty/VendorCategory · Access: Admin, ThirdParty · Master Data
A CRUD screen for vendor category records used as a classification lookup. Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through vendor category records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- The Vendor module references these records through the Vendor Category lookup.
Vendor Group
/ThirdParty/VendorGroup · Access: Admin, ThirdParty · Master Data
A CRUD screen for vendor group records used as a classification lookup. Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through vendor group records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- The Vendor module references these records through the Vendor Group lookup.
Vendor Contact
/ThirdParty/VendorContact · Access: Admin, ThirdParty · With Lookup
A CRUD screen for vendor contact persons: name, job title, phone and e-mail, each linked to a vendor record.
What you can do
- Browse, search and page through vendor contact records (server-side grid).
- Use Create New to add a contact and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Vendor | Reference to the owning Vendor record. |
| Name | Required, up to 100 characters. |
| Job Title / Phone / E-mail | Optional contact identification fields. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name, Job Title, Phone Number, Email Address and Description.
- Contacts are also maintained inline from the Vendor module through its contacts editor.
Sales
The Sales area covers the full revenue cycle: Sales Quotation → Sales Order → Delivery Order → Invoice → Payment Receive, with Sales Return and Credit Note handling post-sale returns and adjustments. Delivery documents move stock out of inventory, and confirmed documents feed the CRM dashboard figures. Access requires the Admin or Sales role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Sales Quotation | /Sales/SalesQuotation | Master-Detail | Priced quotation issued to a customer; can be marked as Ordered |
| Sales Order | /Sales/SalesOrder | Master-Detail | Confirmed customer order; invoices read their lines from it |
| Delivery Order | /Sales/DeliveryOrder | Master-Detail | Ships ordered stock to the customer (CUSTOMER system warehouse) |
| Sales Return | /Sales/SalesReturn | Master-Detail | Returns stock from the customer back into a warehouse |
| Credit Note | /Sales/CreditNote | Master-Detail | Adjustment document referencing a confirmed Sales Return |
| Invoice | /Sales/Invoice | Master-Detail | Billing document referencing a confirmed Sales Order |
| Payment Receive | /Sales/PaymentReceive | Master-Detail | Records customer payments against confirmed invoices |
Sales Quotation
/Sales/SalesQuotation · Access: Admin, Sales · Master-Detail
Manages customer quotations with priced lines. Lines are real Sales Quotation Item rows, and every line change recalculates the stored Sub Total, Tax Amount and TOTAL. The status can advance to Ordered once the quotation has been converted into a sales order.
What you can do
- Browse, search and page through quotations (server-side grid).
- Create the header first, then add lines from the Edit page.
- Print the quotation as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; does not drive any downstream document or metric. |
| Cancelled | Voided quotation, kept for history. |
| Confirmed | Accepted by the customer and ready to be ordered. |
| Ordered | The quotation has been converted into a sales order. |
Key properties
| Property | Description |
|---|---|
| Quotation Date | Date the quotation was issued. |
| Customer | Lookup to the Customer module. |
| Tax | Lookup to the Tax module; its percentage drives the tax calculation. |
| Sub Total / Tax / TOTAL | Derived amounts recalculated on every line change (stored as Before Tax, Tax and After Tax amounts). |
Things worth attention
- Auto numbers follow the SQ/{Year}/{Month}/ sequence.
- Tax percentage comes from the selected Tax record; changing it recalculates the totals.
- Lines are saved immediately through their own handlers, so totals are always current.
Sales Order
/Sales/SalesOrder · Access: Admin, Sales · Master-Detail
Manages customer orders with priced lines. Lines are real Sales Order Item rows, and every line change recalculates the stored Sub Total, Tax Amount and TOTAL. Confirmed orders are the source that invoices and payment records read their line details from.
What you can do
- Browse, search and page through orders (server-side grid).
- Create the header first, then add lines from the Edit page.
- Print the order as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; invoices cannot reference it yet. |
| Cancelled | Voided order, kept for history. |
| Confirmed | Committed order; offered in the Invoice and Delivery Order lookups. |
| Archived | Closed order retained for reference. |
Key properties
| Property | Description |
|---|---|
| Order Date | Date the order was placed. |
| Customer | Lookup to the Customer module. |
| Tax | Lookup to the Tax module; its percentage drives the tax calculation. |
| Sub Total / Tax / TOTAL | Derived amounts recalculated on every line change. |
Things worth attention
- Auto numbers follow the SO/{Year}/{Month}/ sequence.
- Invoices and payments only offer orders with status Confirmed in their reference lookups.
- Pending-payment metrics on the CRM dashboard sum confirmed invoices that are not fully paid.
Delivery Order
/Sales/DeliveryOrder · Access: Admin, Sales · Master-Detail
Manages the Delivery Order inventory document. Lines are stored as Inventory Transaction rows and stock is recalculated through the shared inventory engine: stock leaves the selected physical warehouse and is posted to the CUSTOMER system warehouse.
What you can do
- Browse, search and page through Delivery Order documents (search matches Auto Number, Description and Status).
- Create the header, then add lines from the Edit page; each line picks its own warehouse.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Lines exist but do not affect stock. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock (movements posted to the CUSTOMER warehouse). |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Delivery Date | Date of the shipment. |
| Sales Order | Reference to the originating sales order. |
| Lines | Inventory Transaction rows tagged with module DeliveryOrder (code DO); product, quantity and warehouse per line. |
Things worth attention
- Only lines on documents with status Confirmed count toward stock.
- Each line picks its own warehouse; stock leaves that warehouse to the CUSTOMER system warehouse.
- Sales Returns reference the delivery document they reverse.
Sales Return
/Sales/SalesReturn · Access: Admin, Sales · Master-Detail
Manages the Sales Return inventory document, which brings stock back from the customer. Lines are stored as Inventory Transaction rows (module code SRN) and each line selects the warehouse that receives the returned stock.
What you can do
- Browse, search and page through Sales Return documents (search matches Auto Number, Description and Status).
- Create the header, then add lines from the Edit page.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; no stock effect. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock and the return can back a credit note. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Return Date | Date the goods were returned. |
| Delivery Order | Reference to the delivery document being reversed. |
| Lines | Inventory Transaction rows (code SRN) with product, quantity and receiving warehouse. |
Things worth attention
- Only lines on documents with status Confirmed count toward stock.
- Credit notes only offer confirmed sales returns in their reference lookup.
Credit Note
/Sales/CreditNote · Access: Admin, Sales · Master-Detail
Manages credit notes raised from confirmed sales returns. Credit notes store no lines of their own: the lines and totals are always read from the referenced Sales Return, so the adjustment can never drift from the return it credits.
What you can do
- Browse, search and page through credit notes (server-side grid).
- Select a confirmed Sales Return when creating a credit note; its lines and totals are previewed.
- Print the credit note as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy referencing a sales return. |
| Cancelled | Voided credit note, kept for history. |
| Confirmed | Approved adjustment; payment records can reference it. |
| Archived | Closed adjustment retained for reference. |
Things worth attention
- Auto numbers follow the CN/{Year}/{Month}/ sequence.
- Only Sales Returns with status Confirmed are offered in the reference lookup.
- Payment records reference confirmed credit notes.
Invoice
/Sales/Invoice · Access: Admin, Sales · Master-Detail
Manages invoices raised from confirmed sales orders. Invoices store no lines of their own: the lines and totals are always read from the referenced Sales Order. The invoice status additionally tracks payment progress with Partial Paid and Full Paid states.
What you can do
- Browse, search and page through invoices (server-side grid).
- Select a confirmed Sales Order when creating an invoice; its lines and totals are previewed.
- Print the invoice as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; payment records cannot reference it. |
| Cancelled | Voided invoice, kept for history. |
| Confirmed | Issued invoice; offered in the Payment Receive lookup. |
| Partial Paid | Some payment has been received (maintained manually). |
| Full Paid | Invoice is fully settled; excludes it from pending-payment metrics. |
Things worth attention
- Auto numbers follow the INV/{Year}/{Month}/ sequence.
- Only Sales Orders with status Confirmed are offered in the reference lookup.
- Recording a payment does not change the invoice status; the status is maintained manually.
Payment Receive
/Sales/PaymentReceive · Access: Admin, Sales · Master-Detail
Records customer payments against confirmed invoices. A payment stores its own amount, date, method and status; the invoice lines and totals shown for reference are always read from the invoice's Sales Order.
What you can do
- Browse, search and page through payment records (server-side grid).
- Select a confirmed Invoice when creating a payment; the suggested amount defaults to the Sales Order total.
- Print the payment receipt as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Recorded payment not yet applied. |
| Cancelled | Voided payment, kept for history. |
| Confirmed | Payment accepted against the invoice. |
| Archived | Closed record retained for reference. |
Key properties
| Property | Description |
|---|---|
| Invoice | Reference to the invoice being settled. |
| Payment Date | Date the payment was received. |
| Payment Method | Lookup to the Payment Method module. |
| Payment Amount | Amount received; defaults to the order total for convenience. |
Things worth attention
- Auto numbers follow the PAY/{Year}/{Month}/ sequence.
- Only invoices with status Confirmed are offered in the reference lookup.
- Payment records do not change the invoice status (status is maintained manually).
Purchase
The Purchase area covers the full procurement cycle: Purchase Requisition → Purchase Order → Goods Receive → Bill → Payment Disburse, with Purchase Return and Debit Note handling returns to vendors and subsequent adjustments. Goods Receive posts stock into inventory, and bills feed the payable side of the dashboard. Access requires the Admin or Purchase role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Purchase Requisition | /Purchase/PurchaseRequisition | Master-Detail | Internal request to buy from a vendor, with priced lines |
| Purchase Order | /Purchase/PurchaseOrder | Master-Detail | Committed vendor order; receipts and bills read from it |
| Goods Receive | /Purchase/GoodsReceive | Master-Detail | Receives ordered stock from the VENDOR system warehouse |
| Purchase Return | /Purchase/PurchaseReturn | Master-Detail | Returns stock to the vendor from a selected warehouse |
| Debit Note | /Purchase/DebitNote | Master-Detail | Adjustment document referencing a confirmed Purchase Return |
| Bill | /Purchase/Bill | Master-Detail | Vendor billing document referencing a confirmed Purchase Order |
| Payment Disburse | /Purchase/PaymentDisburse | Master-Detail | Records payments issued against confirmed bills |
Purchase Requisition
/Purchase/PurchaseRequisition · Access: Admin, Purchase · Master-Detail
Manages vendor requisitions with priced lines. Lines are real Purchase Requisition Item rows, and every line change recalculates the stored Sub Total, Tax Amount and TOTAL. The status can advance to Ordered once the requisition has been turned into a purchase order.
What you can do
- Browse, search and page through requisitions (server-side grid).
- Create the header first, then add lines from the Edit page.
- Print the requisition as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; does not drive any downstream document. |
| Cancelled | Voided requisition, kept for history. |
| Confirmed | Approved request ready to be ordered. |
| Ordered | The requisition has been converted into a purchase order. |
Key properties
| Property | Description |
|---|---|
| Requisition Date | Date the requisition was raised. |
| Vendor | Lookup to the Vendor module. |
| Tax | Lookup to the Tax module; its percentage drives the tax calculation. |
| Sub Total / Tax / TOTAL | Derived amounts recalculated on every line change. |
Things worth attention
- Auto numbers follow the PR/{Year}/{Month}/ sequence.
- Tax percentage comes from the selected Tax record; changing it recalculates the totals.
- Lines are saved immediately through their own handlers, so totals are always current.
Purchase Order
/Purchase/PurchaseOrder · Access: Admin, Purchase · Master-Detail
Manages vendor orders with priced lines. Lines are real Purchase Order Item rows, and every line change recalculates the stored Sub Total, Tax Amount and TOTAL. Confirmed orders are the source that goods receipts and bills read their line details from.
What you can do
- Browse, search and page through orders (server-side grid).
- Create the header first, then add lines from the Edit page.
- Print the order as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; receipts and bills cannot reference it yet. |
| Cancelled | Voided order, kept for history. |
| Confirmed | Committed order; offered in the Goods Receive and Bill lookups. |
| Archived | Closed order retained for reference. |
Key properties
| Property | Description |
|---|---|
| Order Date | Date the order was placed with the vendor. |
| Vendor | Lookup to the Vendor module. |
| Tax | Lookup to the Tax module; its percentage drives the tax calculation. |
| Sub Total / Tax / TOTAL | Derived amounts recalculated on every line change. |
Things worth attention
- Auto numbers follow the PO/{Year}/{Month}/ sequence.
- Bills only offer purchase orders with status Confirmed in their reference lookup.
Goods Receive
/Purchase/GoodsReceive · Access: Admin, Purchase · Master-Detail
Manages the Goods Receive inventory document. Lines are stored as Inventory Transaction rows (module code GR) and stock is recalculated through the shared inventory engine: each line receives stock into a physical warehouse from the VENDOR system warehouse.
What you can do
- Browse, search and page through Goods Receive documents (search matches Auto Number, Description and Status).
- Create the header, then add lines from the Edit page; each line picks the receiving warehouse.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Lines exist but do not affect stock. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Receive Date | Date the goods arrived. |
| Purchase Order | Reference to the originating purchase order. |
| Lines | Inventory Transaction rows (code GR) with product, quantity and receiving warehouse. |
Things worth attention
- Only lines on documents with status Confirmed count toward stock.
- Purchase Returns reference the goods receipt they reverse.
Purchase Return
/Purchase/PurchaseReturn · Access: Admin, Purchase · Master-Detail
Manages the Purchase Return inventory document, which sends stock back to the vendor. Lines are stored as Inventory Transaction rows (module code PRN) and each line selects the warehouse the returned stock leaves.
What you can do
- Browse, search and page through Purchase Return documents (search matches Auto Number, Description and Status).
- Create the header, then add lines from the Edit page.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; no stock effect. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock and the return can back a debit note. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Return Date | Date the goods were returned to the vendor. |
| Goods Receive | Reference to the receipt being reversed. |
| Lines | Inventory Transaction rows (code PRN) with product, quantity and source warehouse. |
Things worth attention
- Only lines on documents with status Confirmed count toward stock.
- Debit notes only offer confirmed purchase returns in their reference lookup.
Debit Note
/Purchase/DebitNote · Access: Admin, Purchase · Master-Detail
Manages debit notes raised from confirmed purchase returns. Debit notes store no lines of their own: the lines and totals are always read from the referenced Purchase Return, so the adjustment can never drift from the return it debits.
What you can do
- Browse, search and page through debit notes (server-side grid).
- Select a confirmed Purchase Return when creating a debit note; its lines and totals are previewed.
- Print the debit note as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy referencing a purchase return. |
| Cancelled | Voided debit note, kept for history. |
| Confirmed | Approved adjustment; payment records can reference it. |
| Archived | Closed adjustment retained for reference. |
Things worth attention
- Auto numbers follow the DN/{Year}/{Month}/ sequence.
- Only Purchase Returns with status Confirmed are offered in the reference lookup.
- Payment records reference confirmed debit notes.
Bill
/Purchase/Bill · Access: Admin, Purchase · Master-Detail
Manages bills raised from confirmed purchase orders. Bills store no lines of their own: the lines and totals are always read from the referenced Purchase Order. The bill status additionally tracks payment progress with Partial Paid and Full Paid states.
What you can do
- Browse, search and page through bills (server-side grid).
- Select a confirmed Purchase Order when creating a bill; its lines and totals are previewed.
- Print the bill as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; payment records cannot reference it. |
| Cancelled | Voided bill, kept for history. |
| Confirmed | Accepted vendor bill; offered in the Payment Disburse lookup. |
| Partial Paid | Some payment has been issued (maintained manually). |
| Full Paid | Bill is fully settled. |
Things worth attention
- Auto numbers follow the BILL/{Year}/{Month}/ sequence.
- Only Purchase Orders with status Confirmed are offered in the reference lookup.
- Recording a payment does not change the bill status; the status is maintained manually.
Payment Disburse
/Purchase/PaymentDisburse · Access: Admin, Purchase · Master-Detail
Records payments issued to vendors against confirmed bills. A payment stores its own amount, date, method and status; the bill lines and totals shown for reference are always read from the bill's Purchase Order.
What you can do
- Browse, search and page through payment records (server-side grid).
- Select a confirmed Bill when creating a payment; the suggested amount defaults to the order total.
- Print the payment voucher as PDF from the Detail page; export the filtered grid to XLSX from the list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Recorded payment not yet issued. |
| Cancelled | Voided payment, kept for history. |
| Confirmed | Payment issued against the bill. |
| Archived | Closed record retained for reference. |
Key properties
| Property | Description |
|---|---|
| Bill | Reference to the bill being settled. |
| Payment Date | Date the payment was issued. |
| Payment Method | Lookup to the Payment Method module. |
| Payment Amount | Amount paid; defaults to the purchase order total for convenience. |
Things worth attention
- Auto numbers follow the DISB/{Year}/{Month}/ sequence.
- Only bills with status Confirmed are offered in the reference lookup.
- Payment records do not change the bill status (status is maintained manually).
Inventory
The Inventory area is organized in three layers: master data (unit measures, product groups, products, warehouses), stock documents (transfers, adjustments, scrapping, stock count) and reports (stock on hand, movement summary, transaction trace). All stock documents share one inventory engine: lines are Inventory Transaction rows, and only Confirmed lines count toward stock. Access requires the Admin or Inventory role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Unit Measure | /Inventory/UnitMeasure | Master Data | Units of measure referenced by products |
| Product Group | /Inventory/ProductGroup | Master Data | Product grouping referenced by products |
| Product | /Inventory/Product | With Lookup | Product catalog with price, unit measure and group |
| Warehouse | /Inventory/Warehouse | Master Data | Physical and system warehouses used by the stock engine |
| Transfer Out | /Inventory/TransferOut | Master-Detail | Releases stock from a source warehouse for transfer |
| Transfer In | /Inventory/TransferIn | Master-Detail | Receives transferred stock, following a Transfer Out document |
| Positive Adjustment | /Inventory/PositiveAdjustment | Master-Detail | Increases stock (for example found or corrected quantities) |
| Negative Adjustment | /Inventory/NegativeAdjustment | Master-Detail | Decreases stock (for example losses or corrections) |
| Scrapping | /Inventory/Scrapping | Master-Detail | Writes off damaged or unusable stock |
| Stock Count | /Inventory/StockCount | Master-Detail | Physical count whose delta against system stock is posted |
| Stock Report | /Inventory/StockReport | Report | Live on-hand stock per physical warehouse and product |
| Movement Report | /Inventory/MovementReport | Report | Confirmed movements per warehouse/product split by document type |
| Transaction Report | /Inventory/TransactionReport | Report | Every confirmed inventory transaction with its origin document |
Unit Measure
/Inventory/UnitMeasure · Access: Admin, Inventory · Master Data
A CRUD screen for unit measure records (each, piece, box, kilogram...). Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through unit measure records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- The Product module references these records through the Unit Measure lookup.
Product Group
/Inventory/ProductGroup · Access: Admin, Inventory · Master Data
A CRUD screen for product group records used to categorize the catalog. Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through product group records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- The Product module references these records through the Product Group lookup.
Product
/Inventory/Product · Access: Admin, Inventory · With Lookup
The product catalog. Each record stores an auto number, a name, a unit price that acts as the default price on document lines, a physical-product flag, and lookups to the unit measure and product group.
What you can do
- Browse, search and page through product records (server-side grid).
- Use Create New to add a product and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference; the product number used in reports. |
| Name | Required, up to 100 characters. |
| Unit Price | Default price used in sales and purchase document lines. |
| Physical Product | Yes / No flag marking a physical, stock-tracked product. |
| Unit Measure / Product Group | Lookups to the Unit Measure and Product Group modules. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- Products are referenced by sales and purchase document lines and by inventory transactions; only physical products appear in stock reports.
Warehouse
/Inventory/Warehouse · Access: Admin, Inventory · Master Data
Manages the warehouses used by the stock engine. Beside your own physical warehouses, the system seeds six virtual system warehouses that model the counterparty of each stock movement.
What you can do
- Browse, search and page through warehouse records (server-side grid).
- Use Create New to add a physical warehouse and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| System Warehouse | Yes / No flag marking a virtual system warehouse; system warehouses cannot be deleted. |
| Description | Optional long text. |
Things worth attention
- Six system warehouses are seeded automatically: CUSTOMER, VENDOR, TRANSIT, STOCK_COUNT, ADJUSTMENT and SCRAPPING.
- Inventory documents move stock between physical warehouses and these system warehouses.
- Stock reports exclude system warehouses by design.
Transfer Out
/Inventory/TransferOut · Access: Admin, Inventory · Master-Detail
Manages the Transfer Out inventory document. Lines are stored as Inventory Transaction rows (module code TO-OUT), and adding, editing or deleting a line posts its transaction immediately so stock stays correct.
What you can do
- Browse, search and page through Transfer Out documents (search matches Auto Number, Description and Status).
- Create the header with its source and destination warehouses, then add lines from the Edit page.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Line transactions exist but do not affect stock. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Transfer Release Date | Date the stock is released. |
| Warehouse From / Warehouse To | Source and destination warehouses for the transfer. |
| Lines | Inventory Transaction rows (code TO-OUT) with product and quantity. |
Things worth attention
- Only lines on documents with status Confirmed count toward stock.
- Transfer In documents are created against a confirmed Transfer Out.
Transfer In
/Inventory/TransferIn · Access: Admin, Inventory · Master-Detail
Manages the Transfer In inventory document, which receives stock at the destination warehouse. Lines are stored as Inventory Transaction rows (module code TO-IN) and the destination warehouse follows the referenced Transfer Out document.
What you can do
- Browse, search and page through Transfer In documents (search matches Auto Number, Description and Status).
- Create the header against a Transfer Out document, then add lines from the Edit page.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; no stock effect. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock at the destination warehouse. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Transfer Receive Date | Date the stock is received. |
| Transfer Out | Reference to the originating Transfer Out document. |
| Lines | Inventory Transaction rows (code TO-IN) with product and quantity. |
Things worth attention
- Only lines on documents with status Confirmed count toward stock.
- Transfers count once the outbound and inbound documents are both confirmed.
Positive Adjustment
/Inventory/PositiveAdjustment · Access: Admin, Inventory · Master-Detail
Manages the Positive Adjustment inventory document, which increases stock. Lines are stored as Inventory Transaction rows (module code ADJ+). The document header has no warehouse, so each line selects its own warehouse.
What you can do
- Browse, search and page through Positive Adjustment documents (search matches Auto Number, Description and Status).
- Create the header, then add lines from the Edit page.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; no stock effect. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Adjustment Date | Date of the adjustment. |
| Lines | Inventory Transaction rows (code ADJ+) with product, quantity and receiving warehouse. |
Things worth attention
- Each line selects its own warehouse because the document header has no warehouse.
- Only lines on documents with status Confirmed count toward stock.
Negative Adjustment
/Inventory/NegativeAdjustment · Access: Admin, Inventory · Master-Detail
Manages the Negative Adjustment inventory document, which decreases stock. Lines are stored as Inventory Transaction rows (module code ADJ-); the movement is stored as an absolute value and the direction comes from the module itself.
What you can do
- Browse, search and page through Negative Adjustment documents (search matches Auto Number, Description and Status).
- Create the header, then add lines from the Edit page.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; no stock effect. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Adjustment Date | Date of the adjustment. |
| Lines | Inventory Transaction rows (code ADJ-) with product, quantity and source warehouse. |
Things worth attention
- Movement is stored as an absolute value; the direction comes from the module.
- Only lines on documents with status Confirmed count toward stock.
Scrapping
/Inventory/Scrapping · Access: Admin, Inventory · Master-Detail
Manages the Scrapping inventory document, which writes stock off. Lines are stored as Inventory Transaction rows (module code SCRP); the movement is stored as an absolute value and the direction comes from the module.
What you can do
- Browse, search and page through Scrapping documents (search matches Auto Number, Description and Status).
- Create the header with its warehouse, then add lines from the Edit page.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; no stock effect. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | Lines count toward stock. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Scrapping Date | Date of the write-off. |
| Warehouse | Warehouse the scrapped stock leaves. |
| Lines | Inventory Transaction rows (code SCRP) with product and quantity. |
Things worth attention
- Movement is stored as an absolute value; the direction comes from the module.
- Only lines on documents with status Confirmed count toward stock.
Stock Count
/Inventory/StockCount · Access: Admin, Inventory · Master-Detail
Manages the Stock Count inventory document, which reconciles physical counts with system stock. Lines are stored as Inventory Transaction rows (module code COUNT): the system quantity is computed from confirmed stock and the delta becomes the posted movement.
What you can do
- Browse, search and page through Stock Count documents (search matches Auto Number, Description and Status).
- Create the header with its warehouse, then record counted quantities on the lines.
- Print PDF from the Detail page and Export XLSX for the filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Draft | Working copy; no stock effect. |
| Cancelled | Voided; excluded from stock. |
| Confirmed | The counted delta is posted to stock. |
| Archived | Closed document retained for history. |
Key properties
| Property | Description |
|---|---|
| Count Date | Date of the physical count. |
| Warehouse | Warehouse being counted. |
| Lines | Inventory Transaction rows (code COUNT); system quantity versus counted quantity, with the delta posted. |
Things worth attention
- System quantity is computed from confirmed stock; the delta becomes the posted movement.
- Only lines on documents with status Confirmed count toward stock.
Stock Report
/Inventory/StockReport · Access: Admin, Inventory · Report
Shows current on-hand stock per physical warehouse and product. Only Confirmed inventory transactions count, and system warehouses are excluded so the figures reflect real inventory.
What you can do
- Check the live stock position for every product in every physical warehouse.
- Search by warehouse, product number or product name; Export XLSX downloads the filtered report.
Key columns
| Column | Description |
|---|---|
| Warehouse | Physical warehouse; the six system warehouses are excluded. |
| Product Number / Name | Physical products only; the number is the product auto number. |
| Stock | Sum of signed stock movements (In positive, Out negative) across confirmed transactions. |
Things worth attention
- Draft or cancelled documents do not affect this report.
- Transfers count once the outbound and inbound documents are confirmed.
Movement Report
/Inventory/MovementReport · Access: Admin, Inventory · Report
Summarizes confirmed stock movements per warehouse and product, split into one column per document type: ADJ+, ADJ-, COUNT, DO, GR, PRN, SCRP, SRN, TO-IN and TO-OUT.
What you can do
- Understand where stock changed and by which document type.
- Search by warehouse, product number or product name; Export XLSX downloads the filtered grid.
Things worth attention
- Only Confirmed transactions count; system warehouses are excluded.
- Grand Total is the sum of all module columns for the row.
Transaction Report
/Inventory/TransactionReport · Access: Admin, Inventory · Report
Lists every confirmed inventory transaction with its document reference, product, warehouse routing, signed movement and resulting stock value - the audit trail behind the Stock and Movement reports.
What you can do
- Trace exactly which document produced a stock movement and when.
- Search by auto number, module code, module number, warehouse or product; Export XLSX downloads the filtered grid.
Key columns
| Column | Description |
|---|---|
| Module Code / Number | Origin document kind (DO, GR, SRN, PRN, ADJ+, ADJ-, COUNT, SCRP, TO-IN, TO-OUT) and its document number. |
| Movement / Type / Stock | Movement is the line quantity; Type is In or Out; Stock is the signed effect (Movement x direction). |
| From / To Warehouse | Physical or system warehouse routing recorded by the inventory engine. |
Things worth attention
- Only Confirmed transactions appear.
- Line rows are created instantly from their document editors; deleting a line removes it from this report.
Utilities
The Utilities area contains scheduling and general-purpose modules: bookings with their groups and resources, the Program Manager work tracker with its resources, and the Payment Method lookup used by both payment modules. Access requires the Admin or Utilities role.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Booking Group | /Utilities/BookingGroup | Master Data | Grouping lookup for booking resources |
| Booking Resource | /Utilities/BookingResource | With Lookup | Bookable resources (rooms, equipment, people) with a group |
| Booking | /Utilities/Booking | With Lookup | Scheduler records with start/end times, status and recurrence fields |
| Program Manager | /Utilities/ProgramManager | With Lookup | Work items with status, priority, summary and a resource |
| Program Resource | /Utilities/ProgramManagerResource | Master Data | Resource lookup referenced by program manager records |
| Payment Method | /Utilities/PaymentMethod | Master Data | Payment method lookup used by payments in and out |
Booking Group
/Utilities/BookingGroup · Access: Admin, Utilities · Master Data
A CRUD screen for booking group records used to organize bookable resources. Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through booking group records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- Booking Resource records reference these groups through the Booking Group lookup.
Booking Resource
/Utilities/BookingResource · Access: Admin, Utilities · With Lookup
A CRUD screen for bookable resources: rooms, equipment or people that bookings can be scheduled against. Each resource belongs to a booking group.
What you can do
- Browse, search and page through booking resource records (server-side grid).
- Use Create New to add a resource and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Booking Group | Reference to the Booking Group record. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- Bookings reference these resources through the Booking Resource lookup.
Booking
/Utilities/Booking · Access: Admin, Utilities · With Lookup
A CRUD screen for booking records. Each booking stores scheduler data - subject, resource, start and end times, all-day flag and recurrence fields - against a booking resource. The template has no calendar component, so bookings are managed as rich CRUD records with all scheduler fields preserved.
What you can do
- Browse, search and page through booking records (server-side grid).
- Use Create New to add a booking and Export XLSX to download the currently filtered list.
Status lifecycle
| Status | Meaning |
|---|---|
| Cancelled | Booking was cancelled. |
| Draft | Booking being prepared. |
| Confirmed | Booking is confirmed. |
| OnProgress | Booking is currently happening. |
| Done | Booking completed. |
Key properties
| Property | Description |
|---|---|
| Subject | Required, up to 100 characters. |
| Booking Resource | Reference to the booked resource. |
| Start / End Time | Scheduler start and end date and time, with optional timezones. |
| All Day / Read Only / Block Time | Yes / No scheduler flags. |
| Location | Optional free-text location. |
| Recurrence fields | Optional recurrence rule, recurrence ID, following ID and recurrence exception. |
Things worth attention
- Search matches Auto Number, Subject, Status, Location, timezones, Description and the recurrence fields.
- The V4 template has no calendar component; scheduling data is kept complete so a calendar view can be layered on later.
Program Manager
/Utilities/ProgramManager · Access: Admin, Utilities · With Lookup
A CRUD screen for tracked work items: each record has a title, status, priority, summary and a reference to the program resource that carries it.
What you can do
- Browse, search and page through program manager records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Status and priority
| Field | Values |
|---|---|
| Status | Cancelled, Draft, Confirmed, OnProgress, Done. |
| Priority | Low, High, Normal, Critical. |
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Title | Required, up to 100 characters. |
| Program Manager Resource | Reference to the program resource record. |
| Summary | Optional free text. |
Things worth attention
- Search matches Auto Number, Title, Status, Priority and Summary.
- Program Manager records follow the shared AutoNumber rules on every path (runtime and seeding).
Program Resource
/Utilities/ProgramManagerResource · Access: Admin, Utilities · Master Data
A CRUD screen for program resource records: the people or teams that program manager items are assigned to. Each record stores an auto number, a name and an optional description.
What you can do
- Browse, search and page through program resource records (server-side grid).
- Use Create New to add a record and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- Program Manager records reference these resources through the Program Resource lookup.
Payment Method
/Utilities/PaymentMethod · Access: Admin, Utilities · Master Data
A CRUD screen for payment method records used when recording money in and out. Five methods are seeded by default: Credit Card, Debit Card, Bank Transfer, PayPal and Cash.
What you can do
- Browse, search and page through payment method records (server-side grid).
- Use Create New to add a method and Export XLSX to download the currently filtered list.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters. |
| Description | Optional long text. |
Things worth attention
- Search matches Auto Number, Name and Description.
- Payment Receive and Payment Disburse reference these records through the Payment Method lookup.
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 tenant-scoped administrative backbone: identity and role management, reference data (tax, countries, currencies), the auto-number engine, the company profile, and the branch master. System-wide screens (tenants, tenant users, system users, monitoring, logs, deployment files and jobs) live in the Super Admin area instead. 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 | Tax rates used by sales and purchase document lines |
| 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 |
| Branch | /Admin/Branch | Configuration | Branch master with protected Head Office and in-use delete protection |
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.
- Eleven roles are seeded: Guest, Member, Admin, AI, SuperAdmin, Main, Sales, Purchase, Inventory, ThirdParty and Utilities.
- 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 used as reference/master data. Each record carries an auto number, a unique code, a name, a percentage and an optional description. Document lines read their tax percentage from the selected Tax record.
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.
- Sales and purchase document lines recalculate their totals when the selected tax changes.
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 (documents, 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 each tenant is provisioned with one default record named after the tenant (fallback Company) with the default flag set.
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.
Branch
/Admin/Branch · Access: Admin · Configuration
A CRUD screen for the branch master. Branches are the organizational scoping unit of the application:
every branch-scoped document and operational master carries a BranchId and the forms expose a
Branch selector. A protected Head Office branch is seeded per tenant and cannot be deleted.
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.
- Select a row to reveal Detail and Edit; the Delete action is blocked for Head Office and for branches still in use.
Key properties
| Property | Description |
|---|---|
| Auto Number | Sequence-generated reference. |
| Name | Required, up to 100 characters; shown first when selecting a branch on a document. |
| Is Head Office | System flag for the seeded Head Office branch; it cannot be edited or deleted. |
| Description | Optional free text up to 450 characters. |
Things worth attention
- Search matches Auto Number, Name and Description.
- Deleting a branch is blocked while any document or operational master still references it; Head Office is always blocked.
- Branches are tenant-scoped: each workspace has its own Head Office and its own branch list. See Branch (Multi-Branch) for the scoping rules and the per-tenant backfill.
Looking for the system screens? Health Check, Serilog,
System File and the job dashboard are not Admin modules - they live in the
Super Admin area, where the Job Dashboard links
to the external Hangfire dashboard at /hangfire. See
Background Jobs for the scheduled jobs it manages.
Super Admin
The Super Admin area groups the system-wide screens: tenant workspaces, tenant-user mappings, global system
users, monitoring, logs, deployment files and the job dashboard. Every screen requires the
SuperAdmin role, and - unlike the business areas - these pages are exempt from tenant
selection (TenantRouteRules), so they open with or without a selected workspace.
| Module | Route | Type | Purpose |
|---|---|---|---|
| Tenant | /SuperAdmin/Tenant | Configuration | Create and manage tenant workspaces with automatic data provisioning |
| Tenant User | /SuperAdmin/TenantUser | Identity | User-to-tenant access mappings behind the workspace selection |
| System User | /SuperAdmin/User | Identity | Global sign-in accounts across every tenant |
| Health Check | /SuperAdmin/HealthCheck | Dashboard | Health probes plus in-memory traffic metrics |
| Serilog | /SuperAdmin/Serilog | Utility | Browse, read and download the rolling log files |
| System File | /SuperAdmin/SystemFile | Utility | Read-only views of App Settings, Dockerfile and Docker Compose |
| Job Dashboard | /SuperAdmin/JobDashboard | Utility | Entry point to the external Hangfire dashboard at /hangfire |
Tenant
/SuperAdmin/Tenant · Access: SuperAdmin · Configuration
The tenant workspace registry. Creating a tenant also provisions its initial data (countries, currencies, taxes, a default company, the Head Office branch, the system warehouses and the payment methods), so the workspace is immediately usable. The Detail page adds a Users in this tenant panel that lists every mapped user with a direct link to manage the mapping.
What you can do
- Browse, search and page through tenant records (server-side grid; search matches Name, City or Country).
- Use Create New to provision a new tenant workspace, and Export XLSX to download the currently filtered list.
- Edit Name, Description, Is Active and the address/contact block (Street, City, State, Zip Code, Country, Phone, Fax, E-mail, Website).
- Open Detail to review the profile and the mapped users, or delete the tenant (soft delete).
Key properties
| Property | Description |
|---|---|
| Name | Required, up to 100 characters; unique - a duplicate tenant name is rejected. |
| Is Active | Active workspaces are offered at tenant selection; inactive ones are not. |
| Description | Optional free text up to 500 characters. |
| Address / Contact | Street, city, state, zip and country plus phone, fax, e-mail and website. |
| Audit | Created At/By and Updated At/By tracking; deleted tenants are soft-deleted. |
Things worth attention
- Tenant records are global (not tenant-scoped) and visible only to accounts holding the SuperAdmin role.
- Every user holding the SuperAdmin role is automatically mapped to every tenant - including tenants created later (see the Multitenant section).
- User-to-tenant access is managed in the Tenant User module.
- Deleting a tenant here is a soft delete that hides it from selection.
Tenant User
/SuperAdmin/TenantUser · Access: SuperAdmin · Identity
Maps system users to tenant workspaces. A user can belong to several tenants and picks one after signing in; this mapping controls which workspaces are offered at selection time. A pair of tenant and user can only be mapped once.
What you can do
- Browse, search and page through user-to-tenant mappings (search matches Tenant, System User or Summary).
- Use Create New to grant a user access to a tenant; Edit to change the tenant, user, summary or Is Active.
- Select a row to reveal the user actions: Roles, Avatar, Password, Email and Params (they link to the System User screens).
- Export XLSX to download the currently filtered list.
Things worth attention
- Switch Is Active off to suspend access without deleting the mapping.
- Removing a mapping is a soft delete; the user simply no longer sees that tenant at selection time.
- Every user holding the SuperAdmin role is mapped automatically to every tenant, including new ones.
- Newly registered users receive their own default tenant workspace mapping automatically (see the Multitenant section).
System User
/SuperAdmin/User · Access: SuperAdmin · Identity
The global counterpart of the tenant-scoped Admin User module. It manages the application's sign-in accounts (ASP.NET Identity ApplicationUser records) across every tenant: username, e-mail, full name, avatar, e-mail confirmation state, assigned roles, password and profile parameters. Unlike the Admin module, the list shows all system users, whether or not they are mapped to a tenant.
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, across the whole system.
- Create New makes the account pre-confirmed and active, and returns a randomly generated 12-character password to hand over securely.
- The optional Assign to Tenant lookup maps the new account to a tenant in the same submit; leave it empty to create a user without any tenant mapping.
- Roles are not set at creation; use the Roles action, where assignment replaces the user's whole role set.
- Eleven roles are seeded: Guest, Member, Admin, AI, SuperAdmin, Main, Sales, Purchase, Inventory, ThirdParty and Utilities.
- 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.
- Tenant mappings themselves are managed in the Tenant User module; see Multitenant for the sign-in and selection flow.
Health Check
/SuperAdmin/HealthCheck · Access: SuperAdmin · 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 SuperAdmin-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 does not hold the SuperAdmin role.
- Traffic numbers reset whenever the web process restarts (the store is in-memory only).
Serilog
/SuperAdmin/Serilog · Access: SuperAdmin · Utility
A Super 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
/SuperAdmin/SystemFile · Access: SuperAdmin · 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 (
/SuperAdmin/SystemFile/AppSettings), Dockerfile (/SuperAdmin/SystemFile/Dockerfile) and Docker Compose (/SuperAdmin/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.
Job Dashboard
/SuperAdmin/JobDashboard · Access: SuperAdmin · Utility
The entry point to the external Hangfire dashboard at /hangfire. The page
shows a small card and redirects automatically; the Hangfire dashboard itself is mounted outside the VSA
module pattern, so it has no Module Info drawer.
What you can do
- Open the Hangfire dashboard to inspect recurring, fire-and-forget and delayed jobs, their history and failures.
- Use the manual Open Hangfire Dashboard button if the automatic redirect did not run.
Things worth attention
- The dashboard is guarded by the SuperAdmin role (Hangfire authorization filter) - other accounts cannot open it.
- The recurring serilog-cleanup job (daily at midnight) appears here; see Background Jobs for the scheduled jobs it manages.
5. 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 (SuperAdmin only). Supports recurring, fire-and-forget, and delayed jobs.
| File / Path | Description |
|---|---|
Infrastructures/BackgroundJobs/DI.cs | Hangfire configuration + storage |
Infrastructures/BackgroundJobs/HangfireAuthorizationFilter.cs | SuperAdmin-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": "IndotalentDb",
"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.
Multitenant (Tenants)
Indotalent is a multitenant application: one deployment serves many
tenant workspaces. Every business record carries a TenantId, and
the signed-in user selects exactly one workspace, so each tenant sees only its own data. The
tenant model itself, the identity accounts, and the Super Admin screens are global.
Tenant-scoped vs global data
| Kind | What | Scoping |
|---|---|---|
| Business entities | 60 entity classes implement IMultiTenant (documents, line items, masters, todo attachments) | Scoped - each row carries the workspace TenantId |
| Counter data | AutoNumberSequence rows | Scoped via their own TenantId column (per-workspace counters), without the IMultiTenant interface |
| Workspace registry | Tenant and TenantUser | Global - managed only by Super Admin screens |
| Identity | ASP.NET Identity users and roles | Global - one sign-in account can be mapped to several tenants |
| System screens | Super Admin area (tenants, system users, monitoring, logs, system files, jobs) | Global - exempt from tenant selection |
Isolation and stamping
- Global query filter -
AppDbContextappliesTenantId == CurrentTenantIdto everyIMultiTenantentity, combined with the soft-delete filter. - No tenant selected -
CurrentTenantIdfalls back to the sentinel string"NO_TENANT_SELECTED", so unfiltered reads return zero rows instead of leaking data. - Insert stamping - on save, new entities receive the current
TenantIdautomatically; inserting without a tenant context throws"TenantId not found in the current user session context."(note the asymmetry: reads silently return nothing, writes fail loudly). - Cross-tenant block - changing
TenantIdon an existing row throws"Cross-tenant operation detected: TenantId modifications are not allowed." - Legacy rows - rows with an empty
TenantId(created before tenancy) are stamped with the current tenant on their first update.
public interface IMultiTenant
{
string? TenantId { get; set; }
}Sign-in and workspace selection
Module pages require a selected workspace. After sign-in the user picks one on the
Select Tenant page (/TenantSelection, authentication layout),
which lists the user's active mappings only - inactive tenants and mappings are not offered.
// 1. Sign-in (Identity or Firebase) - no tenant is selected yet
// 2. Guard middleware sees a guarded module request without a valid tenant
// → 302 to /TenantSelection?reason=no-tenant (or invalid-tenant)
// 3. Page lists GET /api/tenant-selection/my-tenants (active mappings only)
// 4. Select → POST /api/tenant-selection/select (cookie scheme only)
// → issues the signed HttpOnly cookie X-Tenant-Token (12 h, Secure, SameSite=Lax)
// 5. TenantCookieClaimsTransformation adds the tenant_id claim to the principal
// (the token must belong to the authenticated user)
// 6. Guard re-validates: tenant exists + IsActive and the mapping is granted
// 7. Auto number counters, query filters and insert stamping all use the claim- Guarded prefixes -
TenantGuardMiddlewareguards/Main,/AI,/Admin,/Sales,/Purchase,/Inventory,/ThirdPartyand/Utilities. - Exemptions -
TenantRouteRulesexemptsTenantSelectionand everySuperAdmin/*page; the same rules drive the BlazorMainLayoutfallback guard (circuit-level), so HTTP and circuit behavior stay aligned. - Change workspace - the user menu offers Change Tenant (back to
/TenantSelection) and Modules (the per-role module landing page at/Identity/Account/Manage/Modules). - API clients - instead of the cookie, a Bearer client calls
POST /api/auth/select-tenantand receives a JWT carrying thetenant_idclaim; the cookie endpoint explicitly rejects Bearer-only calls.
Users and access
- TenantUser mapping - access is a mapping record (tenant + user,
Is Active, summary). A user can be mapped to a tenant only once; turningIs Activeoff suspends access and a soft delete hides the tenant from the selection list. - Super Admins see everything - every user holding the SuperAdmin role is mapped automatically to every tenant, including tenants created later.
- Registration provisioning - a new registration receives the Guest and Member roles and gets its own workspace
"{email} Workspace"with the user mapped as Owner; Firebase sign-up provisions the same way. - Modules landing page - one card per non-Guest role the user holds; without a selected tenant only the SuperAdmin card is shown (its card opens
/SuperAdmin/Tenant/Index).
Provisioning a tenant
- Creating a tenant (Super Admin → Tenant, registration or Firebase sign-up) calls
TenantProvisioningService.CreateTenantWithDataAsync, which seeds the workspace with 8 countries, 8 currencies, 4 taxes (NOTAX, T10, T15, T20), a default company (USD, flagged Default), the protected Head Office branch, 6 system warehouses and 5 payment methods. - The same call ensures the SuperAdmin mappings and, when an initial user is supplied, maps that user as Owner. Every per-tenant seed runs under a temporary tenant-context override that is always restored afterwards.
- Bootstrap tenant -
DatabaseSeedercreates the first tenant only when none exists: the primary demo tenant in demo mode, otherwise the app name. - Demo tenants - with Demo Mode on,
TenantSeederensures four demo tenants (Demo Tenant 1-4: Bandung, Jakarta, Singapore, Kuala Lumpur) andDemoSeederseeds the demo modules and todos per tenant, then backfills branches per tenant.
Files
| File / Path | Description |
|---|---|
Data/Interfaces/IMultiTenant.cs | Marker interface (string? TenantId) implemented by 60 entity classes |
Data/Entities/Tenant.cs | Workspace record: name, description, address/contact block, IsActive |
Data/Entities/TenantUser.cs | User-to-tenant mapping (unique tenant + user, IsActive, summary) |
Infrastructures/Databases/AppDbContext.cs | Global query filter, insert stamping, cross-tenant update block, sentinel tenant |
Infrastructures/Databases/TenantProvisioningService.cs | Per-tenant seeding + SuperAdmin mapping maintenance |
Infrastructures/Databases/DatabaseSeeder.cs | Bootstrap tenant, 11-role seeding, default admin with all roles |
Infrastructures/Databases/Seeder/TenantSeeder.cs | The four demo tenant definitions (find-or-create, idempotent) |
Infrastructures/Databases/Seeder/DemoSeeder.cs | Per-tenant demo seeding loop + per-tenant branch backfill |
Infrastructures/Authentications/TenantAuthConstants.cs | Claim name (tenant_id) and cookie name (X-Tenant-Token) |
Infrastructures/Authentications/TenantTokenService.cs | Issues and validates the 12-hour signed tenant token (reuses the JwtSettings key) |
Infrastructures/Authentications/TenantCookieClaimsTransformation.cs | Bridges the tenant cookie into the tenant_id claim on every authenticate call |
Infrastructures/Authentications/TenantGuardMiddleware.cs | HTTP guard: redirects guarded module pages to /TenantSelection with a reason code |
Infrastructures/Authentications/TenantRouteRules.cs | TenantSelection and SuperAdmin exemptions shared by the middleware and MainLayout |
Infrastructures/Authentications/CurrentUserService.cs | TenantId from the claim, plus the scoped override used while seeding |
Areas/Main/TenantSelection/ | Selection page, CQRS handlers and the /api/tenant-selection/* endpoints |
Areas/Identity/Pages/Account/Manage/Modules.cshtml(.cs) | Modules landing page with the tenant gate (SuperAdmin-only card without a workspace) |
Areas/Identity/Pages/Account/Register.cshtml.cs | Auto-provisions the "{email} Workspace" tenant with an Owner mapping |
Branch (Multi-Branch)
A Branch is the organizational scoping unit of the application. Every
branch-scoped record carries a nullable BranchId, and a protected
Head Office branch is seeded per tenant during provisioning. Records are
assigned a branch per document: the forms expose a Branch selector and the lists show a
Branch column. Branches are tenant-scoped - each workspace has its own branch list - and are
managed at /Admin/Branch.
Tables with BranchId (branch-scoped entities)
| Module | Entities |
|---|---|
| Sales (7) | SalesQuotation, SalesOrder, DeliveryOrder, SalesReturn, CreditNote, Invoice, PaymentReceive |
| Purchase (7) | PurchaseRequisition, PurchaseOrder, GoodsReceive, PurchaseReturn, DebitNote, Bill, PaymentDisburse |
| Inventory (9) | TransferOut, TransferIn, PositiveAdjustment, NegativeAdjustment, Scrapping, StockCount, Product, Warehouse, InventoryTransaction |
| Main (5) | Budget, Campaign, Expense, Lead, LeadActivity |
| Utilities (2) | Booking, ProgramManager |
| Third Party (2) | Customer, Vendor |
Tables without BranchId
| Group | Entities | Reason |
|---|---|---|
| Scope root (1) | Branch | The branch master itself; Head Office is seeded and protected from deletion. |
| Document line items (4) | SalesQuotationItem, SalesOrderItem, PurchaseRequisitionItem, PurchaseOrderItem | Detail rows follow their parent document; the branch is read from the parent header. |
| Contact records (3) | CustomerContact, VendorContact, LeadContact | Child records of Customer/Vendor/Lead; scope is inherited from the parent. |
| Identity and system (3) | ApplicationUser, AutoNumberSequence, Company | Sign-in accounts are global; number counters and the company profile are scoped per tenant - none of them is branch-scoped. |
| Global reference data (3) | Tax, Country, Currency | Admin lookup lists shared by every branch. |
| Shared classification and resource masters (12) | CustomerGroup, CustomerCategory, VendorGroup, VendorCategory, ProductGroup, UnitMeasure, PaymentMethod, BookingGroup, BookingResource, ProgramManagerResource, SalesTeam, SalesRepresentative | Masters and resources used across branches, not tied to one branch. |
| Personal productivity (4) | Todo, TodoItem, TodoImageAttachment, TodoFileAttachment | Personal task data owned by a user (OwnerUserId), not organizational branch documents. |
In total, 32 branch-scoped and 30 non-scoped entity classes out of 62. Any new entity that
implements IHasBranch is picked up automatically.
Behavior
- Head Office is seeded per tenant when the workspace is provisioned (idempotently, so demo tenants are covered on every demo startup), and is protected from deletion.
- The backfill assigns Head Office to every branch-scoped row whose
BranchIdis still empty; it runs per tenant after demo seeding (demo rows included) and is idempotent. - Deleting a branch is blocked while records still reference it: "Branch is in use by existing records and cannot be deleted."; Head Office reports "Head Office branch cannot be deleted."
- A startup fail-fast guard verifies that every
IHasBranchentity hasBranchIdmapped and a Branch foreign key configured. - The Branch column is searchable and sortable in the lists.
Files
| File / Path | Description |
|---|---|
Data/Interfaces/IHasBranch.cs | Marker interface (BranchId) - the single source of truth for branch scope |
Data/Entities/Branch.cs | Branch master record: Name, Description, IsHeadOffice |
Areas/Admin/Branch/ | Branch admin module (list, create, edit, delete) |
Infrastructures/Branches/BranchScopedEntities.cs | Discovers branch-scoped entity types from the EF model |
Infrastructures/Branches/BranchUsageChecker.cs | Reference check that blocks deleting a branch in use |
Infrastructures/Branches/BranchModelGuard.cs | Startup fail-fast validation of the branch model wiring |
Infrastructures/Databases/Seeder/BranchSeeder.cs | Seeds Head Office and backfills rows with no branch |
Infrastructures/Databases/Configurations/{Entity}Configuration.cs | Per-entity Branch foreign key (DeleteBehavior.NoAction) and IX_{Entity}_BranchId index |
Adding a new branch-scoped entity
- Implement
IHasBranchon the entity: declarepublic class {Entity} : BaseEntity, IHasAutoNumber, IHasBranchand addpublic string? BranchId { get; set; }pluspublic Branch? Branch { get; set; }. - Configure the relationship and index in the entity's configuration file (snippet below).
- Done - discovery, backfill, the startup guard and delete protection cover the new entity automatically.
builder.HasIndex(e => e.BranchId)
.HasDatabaseName("IX_{Entity}_BranchId");
builder.HasOne(e => e.Branch)
.WithMany()
.HasForeignKey(e => e.BranchId)
.OnDelete(DeleteBehavior.NoAction);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/Seeder/TenantSeeder.cs | Ensures the four demo tenants (Demo Tenant 1-4: Bandung, Jakarta, Singapore, Kuala Lumpur) |
Infrastructures/Databases/Seeder/DemoSeeder.cs | Seeds the demo modules, users and todos per tenant, then backfills branches per tenant |
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 ensures the four demo
tenants and seeds the demo modules, users and todos per tenant on every
startup: each tenant's rows are stamped with its own TenantId and pointed at
its own Head Office branch. 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 /SuperAdmin/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 Super Admin dashboard at /SuperAdmin/HealthCheck/Index shows the health cards plus an
Application card, a rolling Traffic summary, and the most recent endpoints.
It is fed by the SuperAdmin-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/SuperAdmin/HealthCheck/Endpoints/OpsEndpoint.cs | GET /api/ops/metrics minimal API (SuperAdmin 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/min |
| Authenticated | Authenticated users | 200 req/min |
| Write | POST/PUT/DELETE | 50 req/min |
| Admin | Admin role | 500 req/min |
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.
6. 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; }
}7. 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.
8. 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, workspace shell, Module Information drawer) + WorkspaceTopNav.razor (area dropdowns)
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 4s9. 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".
Indotalent Enterprise Kit - Technical Documentation v1.1
Built with ASP.NET Core Blazor Server 10 · MudBlazor · Hangfire · Serilog · EF Core · VSA Architecture