DOCS Technical Documentation

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
ArchitectureVertical Slice via ASP.NET Core Areas
Backend APIMinimal API (retained for HTTP clients, downloads, and OpenAPI)
CQRSPlain handlers (no MediatR dependency)
DatabaseEF Core with multi-provider (InMemory / SQL Server / PostgreSQL)
Primary KeysString (GUID) - no auto-increment
Soft DeleteIHasIsDeleted + global query filter
AuditIHasAudit + auto-populated on SaveChanges
Branch ScopeIHasBranch + BranchId with per-entity FK/index (32 entities)
ValidationFluentValidation (server) + component-side Validate() (UX mirror)
FrontendBlazor Web App (interactive server render mode) + MudBlazor components
UI data accessHandlerInvoker - fresh DI scope per call, circuit user seeded for audit fields
AuthASP.NET Core Identity + JWT with Refresh Token Rotation + Firebase SSO
Rate LimitingSystem.Threading.RateLimiting - 4 policies
CORSConfig-gated via appsettings.json (CorsSettings:IsUsed) - disabled by default
Background JobsHangfire 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 componentsN/A
Areas/Components/Shared dialogs (standard delete confirmation, etc.)N/A
Feature Folder Convention (VSA)

Every feature follows this convention:

Areas/Admin/{EntityName}/
+-- 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 / PathDescription
Areas/Blazor/App.razorDocument shell: renders the app name in the browser title, navbar logo, and global assets
Areas/Blazor/Layouts/MainLayout.razorAuthenticated shell: workspace top navigation, app-bar brand, Module Information drawer
appsettings.json → AppSettingsCentral application name configuration

Configure the application name in appsettings.json under AppSettings:

appsettings.json - 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
MainAdmin, Member, Main11CRM workspace: dashboards, sales teams, campaigns, leads, todo
Third PartyAdmin, ThirdParty8Customer and vendor master data with contact persons
SalesAdmin, Sales7Quotation-to-collection sales cycle
PurchaseAdmin, Purchase7Requisition-to-payment procurement cycle
InventoryAdmin, Inventory13Product master data, stock movements and inventory reports
UtilitiesAdmin, Utilities6Booking, program management and payment method lookups
AIAdmin, AI1Conversational assistant over the configured LLM provider
AdminAdmin8Identity, reference data, sequences, company profile and the branch master
Super AdminSuperAdmin7Tenant workspaces, tenant users, system users, monitoring, logs, deployment files and jobs
Total68

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.

ModuleRouteTypePurpose
CRM Dashboard/Main/CrmDashboardDashboardLive metrics across leads, campaigns, orders, invoices and budgets
Sales Team/Main/SalesTeamMaster DataRegistry of sales teams with an inline members editor
Sales Representative/Main/SalesRepresentativeWith LookupSalespeople with their contact data and team assignment
Campaign/Main/CampaignMaster-DetailMarketing campaigns with inline budgets, expenses and leads panels
Budget/Main/BudgetWith LookupCampaign budget planning records
Expense/Main/ExpenseWith LookupCampaign spending records
Lead/Main/LeadMaster-DetailSales lead with company profile, BANT scores and pipeline stage
Lead Contact/Main/LeadContactWith LookupContact persons attached to a lead
Lead Activity/Main/LeadActivityWith LookupLogged interactions (calls, meetings, e-mails) for a lead
Todo/Main/TodoMaster-DetailTasks with checklist items, attachments, priority and progress
Todo Dashboard/Main/TodoDashboardDashboardRead-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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters; duplicated names are rejected.
MembersSales 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
Job Title / Employee NumberOptional identification fields.
Phone Number / Email AddressOptional contact fields.
Sales TeamLookup 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

StatusMeaning
DraftCampaign being prepared.
CancelledVoided campaign, kept for history.
ConfirmedApproved and ready to run.
OnProgressCampaign is running.
OnHoldTemporarily paused.
FinishedCampaign completed.
ArchivedClosed 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

StatusMeaning
DraftPlanned budget not yet approved.
CancelledVoided budget line, kept for history.
ConfirmedApproved amount counted in campaign spend.
ArchivedClosed record retained for reference.

Key properties

PropertyDescription
Auto NumberSequence-generated reference.
Title / Budget Date / AmountRequired title; date and numeric amount of the budget.
CampaignLookup 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

StatusMeaning
DraftPlanned expense not yet approved.
CancelledVoided expense, kept for history.
ConfirmedApproved spend counted in campaign ROI.
ArchivedClosed record retained for reference.

Key properties

PropertyDescription
Auto NumberSequence-generated reference.
Title / Expense Date / AmountRequired title; date and numeric amount of the expense.
CampaignLookup 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

StageMeaning
1. ProspectingInitial research and outreach.
2. QualificationFit and budget are being qualified.
3. NeedAnalysisCustomer needs are being analyzed.
4. ProposalA proposal has been presented.
5. NegotiationTerms and pricing are being negotiated.
6. DecisionMakingAwaiting the customer decision.
7. ClosedDeal 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

PropertyDescription
TitleRequired, up to 100 characters.
Campaign / Sales TeamOptional lookups to the Campaign and Sales Team modules.
Company profileName, description, address (street, city, state, zip, country), phone, fax, e-mail, website and socials (WhatsApp, LinkedIn, Facebook, Instagram, Twitter).
DatesProspecting date, closing estimation and actual closing date.
AmountsAmount targeted and amount closed; targeted amounts feed the pipeline value metric.
BANT scoresBudget, Authority, Need and Timeline scores, each a percentage between 0 and 100.
Closing NoteOptional 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

PropertyDescription
LeadReference to the owning Lead record.
Full NameRequired, up to 100 characters.
Contact detailsAddress fields, phone, fax, mobile, e-mail and website.
Social profilesWhatsApp, LinkedIn, Facebook, Twitter and Instagram.
AvatarAvatar 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

PropertyDescription
LeadReference to the owning Lead record.
SummaryRequired, up to 100 characters.
TypeOther, Phone, Email, Social Media, Meeting or Event.
From / To DateTime window of the interaction.
Attachment Name / DescriptionOptional 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

PropertyDescription
Name / DescriptionRequired task title (up to 200 characters) and optional longer details.
PriorityOptional Low, Medium or High, rendered as color chips.
CategoryOptional Personal, Work or Learning, each with its own icon.
Due Date / TimeOptional target schedule; the grid displays the date.
ProgressNumeric 0-100 with decimals; the bar is red below 30, amber from 30 to 69 and green from 70 up.
StatusDerived solely from the completed flag: Completed or Pending.
OwnerOptional responsible user; the grid shows the owner's e-mail address.
ItemsChild checklist lines (each with assignee, start date/time and completion state), shown as a badge.
AuditCreated 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.

ModuleRouteTypePurpose
Customer/ThirdParty/CustomerWith LookupCustomer registry with profile and inline contact persons
Customer Category/ThirdParty/CustomerCategoryMaster DataCategory lookup referenced by customers
Customer Group/ThirdParty/CustomerGroupMaster DataGroup lookup referenced by customers
Customer Contact/ThirdParty/CustomerContactWith LookupContact persons attached to a customer
Vendor/ThirdParty/VendorWith LookupVendor registry with profile and inline contact persons
Vendor Category/ThirdParty/VendorCategoryMaster DataCategory lookup referenced by vendors
Vendor Group/ThirdParty/VendorGroupMaster DataGroup lookup referenced by vendors
Vendor Contact/ThirdParty/VendorContactWith LookupContact 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters; duplicated names are rejected.
ProfileAddress fields, country, phone, fax, e-mail, website and socials (WhatsApp, LinkedIn, Facebook, Instagram, Twitter/X, TikTok).
Customer Group / CategoryOptional lookups backed by the Customer Group and Customer Category modules.
ContactsContact 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters; duplicated names are rejected.
DescriptionOptional; 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

PropertyDescription
CustomerReference to the owning Customer record.
NameRequired, up to 100 characters.
Job Title / Phone / E-mailOptional contact identification fields.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters; duplicated names are rejected.
ProfileAddress fields, country, phone, fax, e-mail, website and socials (WhatsApp, LinkedIn, Facebook, Instagram, Twitter/X, TikTok).
Vendor Group / CategoryOptional lookups backed by the Vendor Group and Vendor Category modules.
ContactsContact 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
VendorReference to the owning Vendor record.
NameRequired, up to 100 characters.
Job Title / Phone / E-mailOptional contact identification fields.
DescriptionOptional 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.

ModuleRouteTypePurpose
Sales Quotation/Sales/SalesQuotationMaster-DetailPriced quotation issued to a customer; can be marked as Ordered
Sales Order/Sales/SalesOrderMaster-DetailConfirmed customer order; invoices read their lines from it
Delivery Order/Sales/DeliveryOrderMaster-DetailShips ordered stock to the customer (CUSTOMER system warehouse)
Sales Return/Sales/SalesReturnMaster-DetailReturns stock from the customer back into a warehouse
Credit Note/Sales/CreditNoteMaster-DetailAdjustment document referencing a confirmed Sales Return
Invoice/Sales/InvoiceMaster-DetailBilling document referencing a confirmed Sales Order
Payment Receive/Sales/PaymentReceiveMaster-DetailRecords 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

StatusMeaning
DraftWorking copy; does not drive any downstream document or metric.
CancelledVoided quotation, kept for history.
ConfirmedAccepted by the customer and ready to be ordered.
OrderedThe quotation has been converted into a sales order.

Key properties

PropertyDescription
Quotation DateDate the quotation was issued.
CustomerLookup to the Customer module.
TaxLookup to the Tax module; its percentage drives the tax calculation.
Sub Total / Tax / TOTALDerived 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

StatusMeaning
DraftWorking copy; invoices cannot reference it yet.
CancelledVoided order, kept for history.
ConfirmedCommitted order; offered in the Invoice and Delivery Order lookups.
ArchivedClosed order retained for reference.

Key properties

PropertyDescription
Order DateDate the order was placed.
CustomerLookup to the Customer module.
TaxLookup to the Tax module; its percentage drives the tax calculation.
Sub Total / Tax / TOTALDerived 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

StatusMeaning
DraftLines exist but do not affect stock.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock (movements posted to the CUSTOMER warehouse).
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Delivery DateDate of the shipment.
Sales OrderReference to the originating sales order.
LinesInventory 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

StatusMeaning
DraftWorking copy; no stock effect.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock and the return can back a credit note.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Return DateDate the goods were returned.
Delivery OrderReference to the delivery document being reversed.
LinesInventory 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

StatusMeaning
DraftWorking copy referencing a sales return.
CancelledVoided credit note, kept for history.
ConfirmedApproved adjustment; payment records can reference it.
ArchivedClosed 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

StatusMeaning
DraftWorking copy; payment records cannot reference it.
CancelledVoided invoice, kept for history.
ConfirmedIssued invoice; offered in the Payment Receive lookup.
Partial PaidSome payment has been received (maintained manually).
Full PaidInvoice 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

StatusMeaning
DraftRecorded payment not yet applied.
CancelledVoided payment, kept for history.
ConfirmedPayment accepted against the invoice.
ArchivedClosed record retained for reference.

Key properties

PropertyDescription
InvoiceReference to the invoice being settled.
Payment DateDate the payment was received.
Payment MethodLookup to the Payment Method module.
Payment AmountAmount 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.

ModuleRouteTypePurpose
Purchase Requisition/Purchase/PurchaseRequisitionMaster-DetailInternal request to buy from a vendor, with priced lines
Purchase Order/Purchase/PurchaseOrderMaster-DetailCommitted vendor order; receipts and bills read from it
Goods Receive/Purchase/GoodsReceiveMaster-DetailReceives ordered stock from the VENDOR system warehouse
Purchase Return/Purchase/PurchaseReturnMaster-DetailReturns stock to the vendor from a selected warehouse
Debit Note/Purchase/DebitNoteMaster-DetailAdjustment document referencing a confirmed Purchase Return
Bill/Purchase/BillMaster-DetailVendor billing document referencing a confirmed Purchase Order
Payment Disburse/Purchase/PaymentDisburseMaster-DetailRecords 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

StatusMeaning
DraftWorking copy; does not drive any downstream document.
CancelledVoided requisition, kept for history.
ConfirmedApproved request ready to be ordered.
OrderedThe requisition has been converted into a purchase order.

Key properties

PropertyDescription
Requisition DateDate the requisition was raised.
VendorLookup to the Vendor module.
TaxLookup to the Tax module; its percentage drives the tax calculation.
Sub Total / Tax / TOTALDerived 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

StatusMeaning
DraftWorking copy; receipts and bills cannot reference it yet.
CancelledVoided order, kept for history.
ConfirmedCommitted order; offered in the Goods Receive and Bill lookups.
ArchivedClosed order retained for reference.

Key properties

PropertyDescription
Order DateDate the order was placed with the vendor.
VendorLookup to the Vendor module.
TaxLookup to the Tax module; its percentage drives the tax calculation.
Sub Total / Tax / TOTALDerived 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

StatusMeaning
DraftLines exist but do not affect stock.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Receive DateDate the goods arrived.
Purchase OrderReference to the originating purchase order.
LinesInventory 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

StatusMeaning
DraftWorking copy; no stock effect.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock and the return can back a debit note.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Return DateDate the goods were returned to the vendor.
Goods ReceiveReference to the receipt being reversed.
LinesInventory 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

StatusMeaning
DraftWorking copy referencing a purchase return.
CancelledVoided debit note, kept for history.
ConfirmedApproved adjustment; payment records can reference it.
ArchivedClosed 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

StatusMeaning
DraftWorking copy; payment records cannot reference it.
CancelledVoided bill, kept for history.
ConfirmedAccepted vendor bill; offered in the Payment Disburse lookup.
Partial PaidSome payment has been issued (maintained manually).
Full PaidBill 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

StatusMeaning
DraftRecorded payment not yet issued.
CancelledVoided payment, kept for history.
ConfirmedPayment issued against the bill.
ArchivedClosed record retained for reference.

Key properties

PropertyDescription
BillReference to the bill being settled.
Payment DateDate the payment was issued.
Payment MethodLookup to the Payment Method module.
Payment AmountAmount 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.

ModuleRouteTypePurpose
Unit Measure/Inventory/UnitMeasureMaster DataUnits of measure referenced by products
Product Group/Inventory/ProductGroupMaster DataProduct grouping referenced by products
Product/Inventory/ProductWith LookupProduct catalog with price, unit measure and group
Warehouse/Inventory/WarehouseMaster DataPhysical and system warehouses used by the stock engine
Transfer Out/Inventory/TransferOutMaster-DetailReleases stock from a source warehouse for transfer
Transfer In/Inventory/TransferInMaster-DetailReceives transferred stock, following a Transfer Out document
Positive Adjustment/Inventory/PositiveAdjustmentMaster-DetailIncreases stock (for example found or corrected quantities)
Negative Adjustment/Inventory/NegativeAdjustmentMaster-DetailDecreases stock (for example losses or corrections)
Scrapping/Inventory/ScrappingMaster-DetailWrites off damaged or unusable stock
Stock Count/Inventory/StockCountMaster-DetailPhysical count whose delta against system stock is posted
Stock Report/Inventory/StockReportReportLive on-hand stock per physical warehouse and product
Movement Report/Inventory/MovementReportReportConfirmed movements per warehouse/product split by document type
Transaction Report/Inventory/TransactionReportReportEvery 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference; the product number used in reports.
NameRequired, up to 100 characters.
Unit PriceDefault price used in sales and purchase document lines.
Physical ProductYes / No flag marking a physical, stock-tracked product.
Unit Measure / Product GroupLookups to the Unit Measure and Product Group modules.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
System WarehouseYes / No flag marking a virtual system warehouse; system warehouses cannot be deleted.
DescriptionOptional 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

StatusMeaning
DraftLine transactions exist but do not affect stock.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Transfer Release DateDate the stock is released.
Warehouse From / Warehouse ToSource and destination warehouses for the transfer.
LinesInventory 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

StatusMeaning
DraftWorking copy; no stock effect.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock at the destination warehouse.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Transfer Receive DateDate the stock is received.
Transfer OutReference to the originating Transfer Out document.
LinesInventory 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

StatusMeaning
DraftWorking copy; no stock effect.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Adjustment DateDate of the adjustment.
LinesInventory 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

StatusMeaning
DraftWorking copy; no stock effect.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Adjustment DateDate of the adjustment.
LinesInventory 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

StatusMeaning
DraftWorking copy; no stock effect.
CancelledVoided; excluded from stock.
ConfirmedLines count toward stock.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Scrapping DateDate of the write-off.
WarehouseWarehouse the scrapped stock leaves.
LinesInventory 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

StatusMeaning
DraftWorking copy; no stock effect.
CancelledVoided; excluded from stock.
ConfirmedThe counted delta is posted to stock.
ArchivedClosed document retained for history.

Key properties

PropertyDescription
Count DateDate of the physical count.
WarehouseWarehouse being counted.
LinesInventory 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

ColumnDescription
WarehousePhysical warehouse; the six system warehouses are excluded.
Product Number / NamePhysical products only; the number is the product auto number.
StockSum 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

ColumnDescription
Module Code / NumberOrigin document kind (DO, GR, SRN, PRN, ADJ+, ADJ-, COUNT, SCRP, TO-IN, TO-OUT) and its document number.
Movement / Type / StockMovement is the line quantity; Type is In or Out; Stock is the signed effect (Movement x direction).
From / To WarehousePhysical 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.

ModuleRouteTypePurpose
Booking Group/Utilities/BookingGroupMaster DataGrouping lookup for booking resources
Booking Resource/Utilities/BookingResourceWith LookupBookable resources (rooms, equipment, people) with a group
Booking/Utilities/BookingWith LookupScheduler records with start/end times, status and recurrence fields
Program Manager/Utilities/ProgramManagerWith LookupWork items with status, priority, summary and a resource
Program Resource/Utilities/ProgramManagerResourceMaster DataResource lookup referenced by program manager records
Payment Method/Utilities/PaymentMethodMaster DataPayment 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
Booking GroupReference to the Booking Group record.
DescriptionOptional 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

StatusMeaning
CancelledBooking was cancelled.
DraftBooking being prepared.
ConfirmedBooking is confirmed.
OnProgressBooking is currently happening.
DoneBooking completed.

Key properties

PropertyDescription
SubjectRequired, up to 100 characters.
Booking ResourceReference to the booked resource.
Start / End TimeScheduler start and end date and time, with optional timezones.
All Day / Read Only / Block TimeYes / No scheduler flags.
LocationOptional free-text location.
Recurrence fieldsOptional 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

FieldValues
StatusCancelled, Draft, Confirmed, OnProgress, Done.
PriorityLow, High, Normal, Critical.

Key properties

PropertyDescription
Auto NumberSequence-generated reference.
TitleRequired, up to 100 characters.
Program Manager ResourceReference to the program resource record.
SummaryOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters.
DescriptionOptional 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.

ModuleRouteTypePurpose
Chat/AI/ChatInteractiveConversational 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.

ModuleRouteTypePurpose
User/Admin/UserIdentitySign-in accounts, roles, passwords and profile parameters
Role/Admin/RoleIdentityRead-only list of the application roles
Tax/Admin/TaxMaster DataTax rates used by sales and purchase document lines
Country/Admin/CountryMaster DataCountry reference data
Currency/Admin/CurrencyWith LookupCurrencies linked to countries
Auto Number Sequence/Admin/AutoNumberSequenceConfigurationPer-entity counters behind every auto number
Company/Admin/CompanyConfigurationSingle-record organization profile
Branch/Admin/BranchConfigurationBranch 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

PropertyDescription
AvatarProfile picture rendered from the wwwroot/data/avatar folder; empty values show a gray placeholder.
UsernameThe Identity login name; unique per account.
EmailSign-in e-mail; the app requires a confirmed account at sign-in.
Full NameDisplay name used across the UI.
Email ConfirmedShown as Confirmed (green) or Not Confirmed (red); toggled through the Email action.
AuditCreated 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

PropertyDescription
Auto NumberSequence-generated reference (for example a TAX/2026/0001-style number).
CodeShort unique code; if left blank on create it is back-filled from the generated auto number.
NameRequired, up to 100 characters.
PercentageRate between 0 and 100, shown as a pill with a trailing % sign.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
CodeShort unique code (backed by a unique index); back-filled from the auto number when left blank on create.
NameRequired, up to 100 characters.
DescriptionOptional 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

PropertyDescription
Auto NumberSequence-generated reference.
CodeShort unique code; back-filled from the auto number when left blank on create.
NameRequired, up to 100 characters.
SymbolRequired symbol, up to 5 characters (for example $, € or IDR-style text).
CountryOptional Country lookup; the grid shows the country name or an em dash.
DescriptionOptional 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

PropertyDescription
Entity NameThe record type the sequence feeds (for example Tax, Country, Currency or Company).
Year / MonthCounter period; the current implementation tracks per year only (Month is empty).
Current SequenceThe last issued value; incremented every time a new record is created.
Prefix / Suffix TemplateText around the padded number; may substitute placeholders such as {EntityName}, {Year}, {Year2} and {Month}.
Padding LengthZero-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

PropertyDescription
Auto Number / NameSequence-generated reference and required company name (up to 200 characters).
CurrencyRequired Currency lookup; the grid shows the currency name.
Tax IdentificationTax ID / registration number shown in the grid.
Business License / Description / LogoAdditional profile fields available on Detail and Edit.
AddressStreet, city, state/province and zip code; the grid shows only the city.
ContactPhone and e-mail.
Social / OtherSocial media links (LinkedIn, X, Facebook, Instagram, TikTok) plus Other Information 1-3.
DefaultThe 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

PropertyDescription
Auto NumberSequence-generated reference.
NameRequired, up to 100 characters; shown first when selecting a branch on a document.
Is Head OfficeSystem flag for the seeded Head Office branch; it cannot be edited or deleted.
DescriptionOptional 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.

ModuleRouteTypePurpose
Tenant/SuperAdmin/TenantConfigurationCreate and manage tenant workspaces with automatic data provisioning
Tenant User/SuperAdmin/TenantUserIdentityUser-to-tenant access mappings behind the workspace selection
System User/SuperAdmin/UserIdentityGlobal sign-in accounts across every tenant
Health Check/SuperAdmin/HealthCheckDashboardHealth probes plus in-memory traffic metrics
Serilog/SuperAdmin/SerilogUtilityBrowse, read and download the rolling log files
System File/SuperAdmin/SystemFileUtilityRead-only views of App Settings, Dockerfile and Docker Compose
Job Dashboard/SuperAdmin/JobDashboardUtilityEntry 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

PropertyDescription
NameRequired, up to 100 characters; unique - a duplicate tenant name is rejected.
Is ActiveActive workspaces are offered at tenant selection; inactive ones are not.
DescriptionOptional free text up to 500 characters.
Address / ContactStreet, city, state, zip and country plus phone, fax, e-mail and website.
AuditCreated 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

PropertyDescription
AvatarProfile picture rendered from the wwwroot/data/avatar folder; empty values show a gray placeholder.
UsernameThe Identity login name; unique per account.
EmailSign-in e-mail; the app requires a confirmed account at sign-in.
Full NameDisplay name used across the UI.
Email ConfirmedShown as Confirmed (green) or Not Confirmed (red); toggled through the Email action.
AuditCreated 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

PropertyDescription
File NameToday's file is tagged with an Active badge because it is currently being written.
SizeServer-formatted size shown as a pill (B/KB/MB/GB).
Last ModifiedShown as yyyy-MM-dd HH:mm:ss.
Detail pageReads 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 / PathDescription
Infrastructures/Authentications/Jwt/JwtService.csJWT token generation, refresh token creation, hashing, and validation
Infrastructures/Authentications/Jwt/JwtAuthEndpoints.csMinimal 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.
Configappsettings.json → JwtSettings
JWT + Refresh Token Flow
// 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 DB

Role-Based Authorization

Pre-configured roles: Guest, Member, Admin, AI, Main, Sales, Purchase, Inventory, ThirdParty, Utilities, and SuperAdmin. New users automatically receive the Guest and Member roles.

SuperAdmin administers the whole system (tenants, tenant users, system users, monitoring, logs, and system files), while Admin administers a single tenant workspace.

File / PathDescription
Infrastructures/Authorizations/Identity/ApplicationRoles.csRole constants
Infrastructures/Databases/DatabaseSeeder.csSeeds the default admin and the 11 roles on startup
Areas/Admin/*/Pages/*.razor, Areas/SuperAdmin/*/Pages/*.razor@attribute [Authorize(Roles = ...)]
ApplicationRoles.cs
public static class ApplicationRoles
{
    public const string AdminConst  = "Admin";
    public const string MemberConst = "Member";
    public const string GuestConst  = "Guest";
    public const string SuperAdminConst = "SuperAdmin";
}

SSO 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 / PathDescription
Infrastructures/Authentications/Firebase/Firebase token verification service
appsettings.json → SsoFirebaseFirebase project configuration

To enable Firebase SSO, configure the following in appsettings.json:

appsettings.json - SsoFirebase
"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 / PathDescription
Data/Interfaces/IHasAutoNumber.csInterface definition
Infrastructures/AutoNumberGenerator/AutoNumberGeneratorService.csNumber generation service
UsageAdd : 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 / PathDescription
Infrastructures/BackgroundJobs/DI.csHangfire configuration + storage
Infrastructures/BackgroundJobs/HangfireAuthorizationFilter.csSuperAdmin-only dashboard access
Infrastructures/BackgroundJobs/Jobs/SerilogCleanupJob.csSample 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 / PathDescription
Infrastructures/Databases/DatabaseSettingsModel.csConfiguration model
Infrastructures/Databases/DI.csEF Core provider registration
appsettings.jsonSet "IsUsed": true for your provider

Configure your database provider in appsettings.json under DatabaseSettings:

appsettings.json - 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
KindWhatScoping
Business entities60 entity classes implement IMultiTenant (documents, line items, masters, todo attachments)Scoped - each row carries the workspace TenantId
Counter dataAutoNumberSequence rowsScoped via their own TenantId column (per-workspace counters), without the IMultiTenant interface
Workspace registryTenant and TenantUserGlobal - managed only by Super Admin screens
IdentityASP.NET Identity users and rolesGlobal - one sign-in account can be mapped to several tenants
System screensSuper Admin area (tenants, system users, monitoring, logs, system files, jobs)Global - exempt from tenant selection
Isolation and stamping
  • Global query filter - AppDbContext applies TenantId == CurrentTenantId to every IMultiTenant entity, combined with the soft-delete filter.
  • No tenant selected - CurrentTenantId falls 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 TenantId automatically; 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 TenantId on 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.
Data/Interfaces/IMultiTenant.cs
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.

Tenant selection and guard pipeline
// 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 - TenantGuardMiddleware guards /Main, /AI, /Admin, /Sales, /Purchase, /Inventory, /ThirdParty and /Utilities.
  • Exemptions - TenantRouteRules exempts TenantSelection and every SuperAdmin/* page; the same rules drive the Blazor MainLayout fallback 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-tenant and receives a JWT carrying the tenant_id claim; 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; turning Is Active off 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 - DatabaseSeeder creates the first tenant only when none exists: the primary demo tenant in demo mode, otherwise the app name.
  • Demo tenants - with Demo Mode on, TenantSeeder ensures four demo tenants (Demo Tenant 1-4: Bandung, Jakarta, Singapore, Kuala Lumpur) and DemoSeeder seeds the demo modules and todos per tenant, then backfills branches per tenant.
Files
File / PathDescription
Data/Interfaces/IMultiTenant.csMarker interface (string? TenantId) implemented by 60 entity classes
Data/Entities/Tenant.csWorkspace record: name, description, address/contact block, IsActive
Data/Entities/TenantUser.csUser-to-tenant mapping (unique tenant + user, IsActive, summary)
Infrastructures/Databases/AppDbContext.csGlobal query filter, insert stamping, cross-tenant update block, sentinel tenant
Infrastructures/Databases/TenantProvisioningService.csPer-tenant seeding + SuperAdmin mapping maintenance
Infrastructures/Databases/DatabaseSeeder.csBootstrap tenant, 11-role seeding, default admin with all roles
Infrastructures/Databases/Seeder/TenantSeeder.csThe four demo tenant definitions (find-or-create, idempotent)
Infrastructures/Databases/Seeder/DemoSeeder.csPer-tenant demo seeding loop + per-tenant branch backfill
Infrastructures/Authentications/TenantAuthConstants.csClaim name (tenant_id) and cookie name (X-Tenant-Token)
Infrastructures/Authentications/TenantTokenService.csIssues and validates the 12-hour signed tenant token (reuses the JwtSettings key)
Infrastructures/Authentications/TenantCookieClaimsTransformation.csBridges the tenant cookie into the tenant_id claim on every authenticate call
Infrastructures/Authentications/TenantGuardMiddleware.csHTTP guard: redirects guarded module pages to /TenantSelection with a reason code
Infrastructures/Authentications/TenantRouteRules.csTenantSelection and SuperAdmin exemptions shared by the middleware and MainLayout
Infrastructures/Authentications/CurrentUserService.csTenantId 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.csAuto-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)
ModuleEntities
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
GroupEntitiesReason
Scope root (1)BranchThe branch master itself; Head Office is seeded and protected from deletion.
Document line items (4)SalesQuotationItem, SalesOrderItem, PurchaseRequisitionItem, PurchaseOrderItemDetail rows follow their parent document; the branch is read from the parent header.
Contact records (3)CustomerContact, VendorContact, LeadContactChild records of Customer/Vendor/Lead; scope is inherited from the parent.
Identity and system (3)ApplicationUser, AutoNumberSequence, CompanySign-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, CurrencyAdmin lookup lists shared by every branch.
Shared classification and resource masters (12)CustomerGroup, CustomerCategory, VendorGroup, VendorCategory, ProductGroup, UnitMeasure, PaymentMethod, BookingGroup, BookingResource, ProgramManagerResource, SalesTeam, SalesRepresentativeMasters and resources used across branches, not tied to one branch.
Personal productivity (4)Todo, TodoItem, TodoImageAttachment, TodoFileAttachmentPersonal 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 BranchId is 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 IHasBranch entity has BranchId mapped and a Branch foreign key configured.
  • The Branch column is searchable and sortable in the lists.
Files
File / PathDescription
Data/Interfaces/IHasBranch.csMarker interface (BranchId) - the single source of truth for branch scope
Data/Entities/Branch.csBranch master record: Name, Description, IsHeadOffice
Areas/Admin/Branch/Branch admin module (list, create, edit, delete)
Infrastructures/Branches/BranchScopedEntities.csDiscovers branch-scoped entity types from the EF model
Infrastructures/Branches/BranchUsageChecker.csReference check that blocks deleting a branch in use
Infrastructures/Branches/BranchModelGuard.csStartup fail-fast validation of the branch model wiring
Infrastructures/Databases/Seeder/BranchSeeder.csSeeds Head Office and backfills rows with no branch
Infrastructures/Databases/Configurations/{Entity}Configuration.csPer-entity Branch foreign key (DeleteBehavior.NoAction) and IX_{Entity}_BranchId index
Adding a new branch-scoped entity
  1. Implement IHasBranch on the entity: declare public class {Entity} : BaseEntity, IHasAutoNumber, IHasBranch and add public string? BranchId { get; set; } plus public Branch? Branch { get; set; }.
  2. Configure the relationship and index in the entity's configuration file (snippet below).
  3. Done - discovery, backfill, the startup guard and delete protection cover the new entity automatically.
{Entity}Configuration.cs
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 / PathDescription
Infrastructures/Databases/Seeder/TenantSeeder.csEnsures the four demo tenants (Demo Tenant 1-4: Bandung, Jakarta, Singapore, Kuala Lumpur)
Infrastructures/Databases/Seeder/DemoSeeder.csSeeds the demo modules, users and todos per tenant, then backfills branches per tenant
appsettings.json → DemoModeToggle Demo Mode on/off

Configure Demo Mode in appsettings.json:

appsettings.json - DemoMode
"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 / PathDescription
appsettings.json → AiSettingsAI provider selection and API keys

Configure AI Chat in appsettings.json under AiSettings:

appsettings.json - 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 / PathDescription
Infrastructures/Email/EmailSettingsModel.csProvider selection + API keys
Infrastructures/Email/EmailService.csMain email service with templates
Infrastructures/Email/SendGrid/, Mailgun/, etc.Provider implementations
Infrastructures/Email/IdentityEmailSenderAdapter.csIdentity integration

Configure email delivery in appsettings.json under EmailSettings:

appsettings.json - 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 / PathDescription
Infrastructures/File/FileStorageService.csCore service
Infrastructures/File/FileStorageSettingsModel.csStorage 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 / PathDescription
Infrastructures/HealthChecks/DI.csHealth check registration and endpoint mapping
Program.csError interceptor bypasses /health* so probes keep raw 200/503
appsettings.json - HealthCheckSettingsToggles for the database and storage checks
Endpoints
EndpointPurposeBehavior
/health/liveLiveness (no dependencies)Always 200 while the process is up
/healthzLegacy alias of livenessSame as /health/live
/health/readyReadiness (database + storage)200 when Healthy/Degraded, 503 when Unhealthy
/readyLegacy alias of readinessSame as /health/ready
/healthFull JSON detail for the dashboard200/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.

Dockerfile HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
    CMD curl -f http://localhost:80/health/ready || exit 1

curl 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 / PathDescription
Infrastructures/OpsMetrics/RequestMetricStore.csThread-safe in-memory rolling store (per-minute buckets + bounded latency histogram + recent requests)
Infrastructures/OpsMetrics/RequestMetricsMiddleware.csRecords each request; excludes health probes, static assets, and the metrics endpoint itself
Areas/SuperAdmin/HealthCheck/Endpoints/OpsEndpoint.csGET /api/ops/metrics minimal API (SuperAdmin role)
Infrastructures/OpsMetrics/DI.csAddOpsMetricsService + UseRequestMetricsMiddleware
appsettings.json - OpsMetricsSettingsWindow size, bucket count, recent-request cap

Configure the rolling window in appsettings.json:

appsettings.json - OpsMetricsSettings
"OpsMetricsSettings": {
    "WindowMinutes": 5,
    "MaxBuckets": 10,
    "MaxRecentRequests": 200
}

The snapshot returned by /api/ops/metrics uses camelCase keys:

GET /api/ops/metrics - response shape
{
  "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"
  }
}
  • success counts 2xx responses; errors counts 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
PackagePurpose
OpenTelemetry.Extensions.HostingDI integration (services.AddOpenTelemetry())
OpenTelemetry.Instrumentation.AspNetCoreInbound HTTP server metrics and traces
OpenTelemetry.Instrumentation.HttpOutbound HttpClient metrics and traces
OpenTelemetry.Exporter.OpenTelemetryProtocolOTLP exporter (gRPC by default)
File / PathDescription
Infrastructures/OpenTelemetry/ApiMetrics.csStatic meter Indotalent.Api with counters and a duration histogram
Infrastructures/OpenTelemetry/DI.csAddOpenTelemetryService - only registers providers when enabled
Infrastructures/OpenTelemetry/OpenTelemetrySettingsModel.csSettings model for OpenTelemetrySettings
Infrastructures/DI.csRegistration inside AddInfrastructureDI
appsettings.json - OpenTelemetrySettingsMaster switch + OTLP endpoint and headers

Configure OpenTelemetry in appsettings.json:

appsettings.json - OpenTelemetrySettings
"OpenTelemetrySettings": {
    "Enabled": false,
    "Otlp": {
        "Endpoint": "",
        "Headers": ""
    }
}
  • Both Enabled: true and a non-empty Otlp:Endpoint are required before any exporter starts.
  • Exports the ASP.NET Core and HttpClient instrumentations plus the app meter Indotalent.Api with indotalent.api.requests, indotalent.api.errors, and indotalent.api.request.duration (tag http.response.status.family).
  • The OTLP exporter defaults to the gRPC protocol, so a plain http://host:4317 endpoint 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 the AddOtlpExporter(...) call in Infrastructures/OpenTelemetry/DI.cs.
  • The default service.name resource 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 / PathDescription
Infrastructures/Logging/Serilog/Serilog configuration
wwwroot/data/serilog/Log file output directory
Infrastructures/BackgroundJobs/Jobs/SerilogCleanupJob.csAuto-cleanup job (daily at midnight)

Rate Limiting

Four rate limiting policies using System.Threading.RateLimiting, configurable via appsettings.json.

PolicyScopeDefault
GlobalAll requests100 req/min
AuthenticatedAuthenticated users200 req/min
WritePOST/PUT/DELETE50 req/min
AdminAdmin role500 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 / PathDescription
Infrastructures/Cors/CorsSettingsModel.csSettings model for CorsSettings
Infrastructures/Cors/DI.csAddCorsService / UseCorsService - both no-ops when disabled
Infrastructures/DI.csAddCorsService registration inside AddInfrastructureDI
Program.csUseCorsService between UseRouting and UseAuthorization
appsettings.json - CorsSettingsMaster switch + origin / method / header allow-list
SettingDefaultDescription
IsUsedfalseMaster 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.
AllowCredentialsfalseOnly honored with an explicit origin allow-list (CORS spec forbids allow-any-origin + credentials).

Default configuration in appsettings.json:

appsettings.json - CorsSettings
"CorsSettings": {
    "IsUsed": false,
    "AllowedOrigins": [],
    "AllowedMethods": [],
    "AllowedHeaders": [],
    "ExposedHeaders": [],
    "AllowCredentials": false
}

Example - enabling access for one cross-origin single-page app:

appsettings.json - CorsSettings (example)
"CorsSettings": {
    "IsUsed": true,
    "AllowedOrigins": [ "https://app.example.com" ],
    "AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ],
    "AllowedHeaders": [ "Content-Type", "Authorization" ],
    "AllowCredentials": true
}
  • IsUsed: false means 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.
  • AllowCredentials is 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.json or with the environment variable CorsSettings__IsUsed=true.
  • After enabling, a request from an allowed origin returns Access-Control-Allow-Origin and the preflight OPTIONS request succeeds - verify in the browser DevTools Network tab or with curl -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
GetTaxListHandler.cs
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
CreateTaxHandler.cs
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
DeleteTaxHandler.cs
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:

ApiResponse.cs
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}).

MethodRouteActionAuth
GET/api/{entity}Paginated list with search & sortRequired
GET/api/{entity}/{id}Get by IDRequired
GET/api/{entity}/exportFull Excel export (same search filter)Required
GET/api/{entity}/{lookup}-lookupLookup list for autocompleteRequired
GET/api/{entity}/{id}/itemsMaster-detail items (master-detail features)Required
POST/api/{entity}Create new recordRequired
PUT/api/{entity}Update existing recordRequired
DELETE/api/{entity}/{id}Soft-delete recordRequired

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
Areas/Blazor/
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:

Pages/Index.razor
@* 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).

1 Declarative Index (CrudIndex)
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;
    }
}
2 Bespoke Grid Wiring (Master-Detail / custom)
MudDataGrid ServerData (see Areas/Main/Todo/Pages/Index.razor)
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.

1 Form Model & UI State
Components/TodoModels.cs
// 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, _errors
2 Component Validation (mirrors FluentValidation)
TodoForm.razor - Validate()
public 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();
}
3 Submit with 2000ms Simulated Delay
Pages/Create.razor - SubmitAsync()
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 state fields
// 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 500ms
Example 4: Standard Delete Confirmation Dialog

Delete operations use the shared _DeleteConfirmationDialog component (never confirm()):

Pages/Detail.razor - ConfirmDeleteAsync()
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 OnInitialized
✓ Handlers resolved through HandlerInvoker - never new, never fetch
✓ JS interop only in OnAfterRenderAsync (charts, export, PDF)
✓ Numeric fields use the MudSlider + MudNumericField pair; no JS constants
✓ Shared ui-* CSS classes; no per-page <script>/<style>
✓ Loading state on every async button (Disabled + spinner + text change)
✓ Delete via _DeleteConfirmationDialog (not confirm())
✓ Delays: gate 500ms / submit 2000ms / export 500ms
✓ Success + error via Snackbar; error banners auto-clear after 4s

9. 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)
  1. Fill .ai-assisted/DATA-DICTIONARY.md - application name, persona, and feature description.
  2. Start the sequence - tell your AI coding agent exactly this command:
    start coding
  3. 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).
  4. 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)
✓ The full application, feature by feature (Phase 2)
Entity Types Auto-Detected by AI
Pattern in EntityDetected Type
public ICollection<T>? Items { get; set; }Master-Detail
public string {X}Id { get; set; } + navigation propertyWith Lookup
Neither pattern abovePure Master Data
: BaseEntity, IHasAutoNumberAdds 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:

✓ Entity + EF configuration
✓ 6-8 CQRS handlers + 2 validators
✓ {Entity}Endpoint.cs (feature-scoped group)
✓ 4 Razor pages (Index, Create, Edit, Detail)
✓ {Entity}Fields.cs + {Entity}ModuleInfo.razor
✓ Endpoint registration + top-nav menu item
✓ Master-detail adds Form, ItemsEditor, ItemDialog, Models components
✓ Optional (opt-in): attachments, Print PDF, ECharts dashboard
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.

PURE MASTER DATA

Generate a simple CRUD feature with no relationships:

Generate full CRUD for {Entity}. Follow the skill.
WITH LOOKUP

Generate a feature that references another entity via foreign key:

Generate full CRUD for {Entity} with lookup to {LookupEntity}. Follow the skill.
MASTER-DETAIL

Generate a header-detail feature (e.g., Sales Order with line items):

Generate full CRUD for {MasterEntity} with {DetailEntity}. Follow the skill.
WITH SEED DATA

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:

Limit Context to One Folder

"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.

Reference an Existing Entity

"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.

Avoid Vague Prompts

"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.

Chain Multiple Entities

"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