DOCS Technical Documentation

Indotalent Enterprise Kit Documentation

A comprehensive guide to the architecture, features, and development workflow of the Indotalent ASP.NET Core MVC enterprise starter kit.

Product Overview & Personas

Project Manager (PM) is a resource and project management application that enables organizations to manage staff assignments, project charters, timesheet tracking, and skill profiling — with manager approval workflows and self-service capabilities for staff members.

Product Scope

The platform covers 14 features across 21 entities: organizational master data, staff profiles, project charters, project assignments with allocation tracking, timesheet entry and approval, and a user skill matrix. Master data and operational features are managed centrally, while staff members access a self-service portal limited to their own data.

Personas & Roles
Persona Access Level Scope
Admin Full system access Manage all master data, users, projects, assignments, timesheets, and skills, plus administrative features (user/role management, system configuration).
Member Organization-wide View all data and create/manage projects, assignments, and timesheets for any user. Access the Main menu group.
Guest Own data only Self-service access to own project assignments, timesheets, and skills. Cannot view or modify other users' data.

Master Data Management

The organization's structure and classification data are managed through dedicated master data features. These records form the foundation for staff profiles, project charters, and reporting.

Feature Purpose
BranchOffice locations with code, name, and physical address.
DepartmentBusiness units linked to a branch (e.g., IT Development, Finance & Advisory).
Resource Group / Sub-GroupMulti-level classification of staff specializations (e.g., Software Engineering → Backend Engineer).
Skill Group / SkillSkill taxonomy used by the User Skill Matrix (e.g., Programming Language → C#).
Project Group / Sub-GroupClassification of projects (e.g., Financial Services → Digital Banking Platform).
ClientClient organizations served by projects.

Admin and Member users have full CRUD access to all master data; Guest users have no access.

Expert Directory

Staff profiles extend the standard user account with organizational and costing information, turning every user into a managed resource.

Attribute Description
Branch / DepartmentOffice and business unit assignment.
Resource Sub-GroupSpecialization classification (e.g., Backend Engineer, Data Scientist).
Current StatusAvailability state: Bench Available, Partially Allocated, Fully Allocated, On Leave, Inactive.
Hourly RateStandard internal rate per hour used for costing.

Resource status drives assignment decisions and workload monitoring.

Project Charter

Projects are registered as formal charters, classified by project sub-group and optionally linked to a managing branch and client. Each project follows a lifecycle of predefined states.

Project Lifecycle
State Meaning
Draft PipelineProject is being drafted or in the pipeline.
ActiveProject is actively running.
On HoldProject is temporarily suspended.
CompletedProject has finished successfully.
CancelledProject has been cancelled.

Project charters support image and document attachments (e.g., approval letters, contracts).

Project Assignment & Allocation

Managers allocate expert staff to projects by creating assignments that define the project role, allocation percentage, and effective period. The system validates that a resource's total allocation does not exceed 100% across overlapping periods.

Assignment Lifecycle
State Meaning
DraftAssignment is being prepared.
ActiveResource is allocated to the project.
ReleasedAssignment ended normally (end date reached or project completed).
TerminatedAssignment was terminated prematurely.

Assignments are the basis for timesheet entry and resource workload monitoring.

Timesheet Tracking & Approval

Staff log daily working hours and activity summaries against their project assignments. Submitted entries flow through a manager approval workflow, providing accurate effort data for billing and workload reporting.

Approval Workflow
State Meaning
DraftEntry is being created/edited by the staff member.
SubmittedEntry has been submitted for manager approval.
ApprovedManager has approved the entry.
RejectedManager has rejected the entry; a reason note is recorded.

Only Draft entries can be submitted, and only Submitted entries can be approved or rejected.

User Skill Matrix

The skill matrix links each staff member to the organization's skill taxonomy with a proficiency rating and years of experience. This supports expert discovery and staffing decisions.

Attribute Description
SkillSkill from the skill taxonomy (e.g., C#, React, SQL Server).
Proficiency LevelRating from 1 (Beginner) to 5 (Expert).
Years of ExperienceTotal years of experience with the skill.

Skills can carry supporting image and document attachments (e.g., certificates).

Self-Service Portal

Guest users access a self-service portal covering their own assignments, timesheets, and skills. All operations are automatically scoped to the authenticated user, guaranteeing data isolation.

Menu Capability
My Project AssignmentsView active project allocations assigned to the user.
My TimesheetCreate, edit, and submit own timesheet entries; track approval status.
My Skill MatrixMaintain own skills with proficiency ratings and experience.

The self-service portal keeps staff in control of their day-to-day records while managers retain oversight and approval authority.

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 controller, CQRS handlers, validators, API endpoints, views, and JavaScript. 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 (not MVC controllers for data operations)
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
ValidationFluentValidation (server) + custom JS (client)
FrontendVue 3 Composition API + DataTables (inside MVC views)
AuthASP.NET Core Identity + JWT with Refresh Token Rotation + Firebase SSO
Rate LimitingSystem.Threading.RateLimiting — 4 policies
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 pages (Login, Register, Manage)Mixed
Areas/Admin/Admin-only features (User, Role, Tax, Currency, etc.)Admin role
Areas/Main/Member features (Todo, etc.)Member role
Areas/Components/Reusable partial views (Audit Trail card, etc.)N/A
Feature Folder Convention (VSA)

Every feature follows this convention:

Areas/Admin/{EntityName}/
├── Controllers/{EntityName}Controller.cs
├── 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
└── Views/
    ├── Index.cshtml + Index.cshtml.js
    ├── Create.cshtml + Create.cshtml.js
    ├── Edit.cshtml + Edit.cshtml.js
    └── Detail.cshtml + Detail.cshtml.js

3. Application Name

The application name — displayed in the browser title bar, top-left logo, footer, and sidebar logo — is configured centrally through appsettings.json. This allows you to rebrand the entire application without editing any layout files manually.

File / PathDescription
Areas/Public/Views/Shared/_Layout.cshtmlRenders the app name in the browser title, navbar logo, and footer
Areas/_LayoutArea.cshtmlRenders the app name in the browser title and sidebar logo
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.

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, and Admin. New users automatically receive the Guest role.

File / PathDescription
Infrastructures/Authorizations/Identity/ApplicationRoles.csRole constants
Infrastructures/Databases/DatabaseSeeder.csSeeds Admin user + roles on startup
Areas/Admin/*/Controllers/*.cs[Authorize(Roles = AdminConst)]
ApplicationRoles.cs
public static class ApplicationRoles
{
    public const string AdminConst  = "Admin";
    public const string MemberConst = "Member";
    public const string GuestConst  = "Guest";
}

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 (Admin only). Supports recurring, fire-and-forget, and delayed jobs.

File / PathDescription
Infrastructures/BackgroundJobs/DI.csHangfire configuration + storage
Infrastructures/BackgroundJobs/HangfireAuthorizationFilter.csAdmin-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.

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/DatabaseSeeder.csSeeds demo data when Demo Mode is active
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 will seed dummy data (sample users, roles, and demo records) on every startup. Set "IsDemo": false to disable it and start with a clean database.

AI Chat

Indotalent includes an AI Chat feature that can be enabled by configuring your preferred AI provider's API key. The application supports multiple AI providers including ChatGPT, Claude, Gemini, and DeepSeek.

File / 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 with dashboard UI at /Admin/HealthCheck/Index.

File / PathDescription
Infrastructures/HealthChecks/DI.csHealth check registration
Endpoints/healthz (liveness), /ready (readiness), /health
Dashboard/Admin/HealthCheck/Index

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

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 Taskobject>> HandleAsync(GetTaxListRequest request)
    {
        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));

        int page     = request.Page ?? 1;
        int pageSize = request.PageSize ?? 10;

        var total = await query.CountAsync();
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new TaxListItem { ... })
            .ToListAsync();

        return ApiResponse<object>.Ok(new { items, total, page, pageSize });
    }
}
Step 2: Create Handler
CreateTaxHandler.cs
public class CreateTaxHandler
{
    public async Task> HandleAsync(CreateTaxRequest request)
    {
        // 1. Validate with FluentValidation
        var validator = new CreateTaxValidator();
        var result = await validator.ValidateAsync(request);
        if (!result.IsValid)
            return ApiResponse.Fail(
                "Validation failed", result.ToDictionary());

        // 2. Check for duplicate Code
        if (await _context.Tax.AnyAsync(x => x.Code == request.Code))
            return ApiResponse.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();

        return ApiResponse.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.

Step 4: Delete Handler
DeleteTaxHandler.cs
public class DeleteTaxHandler
{
    public async Taskobject>> HandleAsync(string id)
    {
        var entity = await _context.Tax.FindAsync(id);
        if (entity == null)
            return ApiResponse<object>.Fail("Tax not found");

        _context.Tax.Remove(entity);
        await _context.SaveChangesAsync();

        return ApiResponse<object>.Ok(new { id }, "Tax deleted successfully");
    }
}
Standard API Response

All handlers return ApiResponse which wraps the result:

ApiResponse.cs
public class ApiResponse
{
    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

Data operations use ASP.NET Core Minimal API (not MVC controllers). Each feature registers its endpoints in a single {EntityName}Endpoint.cs file.

MethodRouteActionAuth
GET/api/{entity}Paginated list with search & sortRequired
GET/api/{entity}/{id}Get by IDRequired
POST/api/{entity}Create new recordRequired
PUT/api/{entity}Update existing recordRequired
DELETE/api/{entity}/{id}Delete recordRequired

Endpoints are registered in Program.cs via app.Map{EntityName}Endpoints();.

8. Vue 3 Frontend Tutorial

The frontend uses Vue 3 Composition API with the global build (vue.global.prod.js). Vue is loaded in the layout and each page mounts its own Vue app instance on a specific element. This is not a Single Page Application — Vue enhances specific pages inside ASP.NET Core MVC views.

How Vue is Loaded

In _Layout.cshtml (line ~11), Vue is loaded via a simple script tag:

_Layout.cshtml
// File: _Layout.cshtml (line ~11)
<script src="~/js/vue.global.prod.js"></script>

This exposes the global Vue object. Each page then creates its own app — no build tools, no SPA routing, just lightweight page-level reactivity.

Basic Vue Setup Pattern

Every page that uses Vue follows this pattern:

basic-vue-setup.js
// 1. Destructure Vue APIs you need
const { createApp, ref, reactive, onMounted } = Vue;

// 2. Create and mount a Vue app
createApp({
    setup() {
        // Reactive state (Vue will track changes)
        const contentReady   = ref(false);
        const errorMessage   = ref(null);
        const submitting     = ref(false);

        // Initialize on mount
        onMounted(async function() {
            contentReady.value = true;
        });

        // Return makes these available in HTML template
        return { contentReady, errorMessage, submitting };
    }
}).mount('#app-index');  // Mounts on 
Example 1: DataTable Index Page

This is the pattern used in Areas/Admin/Tax/Views/Index.cshtml.js. It combines Vue with DataTables for server-side paginated tables.

1 Vue Setup for Row Selection
Index.cshtml.js — Vue Setup
const { createApp, ref, onMounted } = Vue;

createApp({
    setup() {
        const contentReady = ref(false);
        const selectedId   = ref(null);

        function selectRow(row, id) {
            selectedId.value = id;
        }

        function clearSelection() {
            selectedId.value = null;
        }

        // Expose to window for DataTables to call
        window.vueApp = { selectRow, clearSelection };

        onMounted(function() {
            setTimeout(function() {
                contentReady.value = true;
            }, 500);
        });

        return { contentReady, selectedId };
    }
}).mount('#app-index');
2 DataTable Initialization
Index.cshtml.js — DataTable
var table = new DataTable('#taxTable', {
    processing:  true,
    serverSide:  true,
    ajax: {
        url: '/api/tax',
        data: function(d) {
            d.search   = d.search?.value || '';
            d.page     = (d.start / d.length) + 1;
            d.pageSize = d.length;
        },
        dataSrc: function(json) {
            if (json.success) {
                json.recordsTotal    = json.data.total;
                json.recordsFiltered = json.data.total;
                return json.data.items;
            }
            return [];
        }
    },
    columns: [
        { data: 'code' },
        { data: 'name' },
        {
            data: 'percentageValue',
            render: function(data) {
                return '' + data + '%';
            }
        }
    ],
    pageLength: 10
});

// Row click / draw handlers
table.on('draw', function() {
    if (window.vueApp) window.vueApp.clearSelection();
});
Example 2: Create Form with Validation

This is the pattern used in Areas/Admin/Tax/Views/Create.cshtml.js.

1 Form State & Reactivity
Create.cshtml.js — Form Setup
const { createApp, ref, reactive } = Vue;

createApp({
    setup() {
        // Form data (reactive object)
        const form = reactive({
            code: '',
            name: '',
            percentageValue: '',
            description: ''
        });

        // Validation errors (reactive)
        const errors = reactive({});

        // UI state
        const submitting = ref(false);
        const created    = ref(false);
        const errorMessage = ref('');

        return { form, errors, submitting, created, errorMessage };
    }
}).mount('#app-create');
2 Client-Side Validation
Create.cshtml.js — Validation
function validate() {
    // Clear previous errors
    Object.keys(errors).forEach(key => delete errors[key]);
    errorMessage.value = '';

    if (!form.code || !form.code.trim()) {
        errors.code = 'Tax Code is required';
    } else if (form.code.length > 50) {
        errors.code = 'Tax Code must not exceed 50 characters';
    }

    if (!form.name || !form.name.trim()) {
        errors.name = 'Tax Name is required';
    }

    const val = parseFloat(form.percentageValue);
    if (isNaN(val) || val < 0 || val > 100) {
        errors.percentageValue = 'Percentage must be between 0 and 100';
    }

    return Object.keys(errors).length === 0;
}
3 Submit with 500ms Smooth Delay
Create.cshtml.js — Submit
async function submitForm() {
    if (!validate()) return;

    submitting.value = true;

    try {
        // Smooth UI delay: 500ms before actual request
        await new Promise(r => setTimeout(r, 500));

        const response = await fetch('/api/tax', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                code:            form.code,
                name:            form.name,
                percentageValue: parseFloat(form.percentageValue),
                description:     form.description
            })
        });

        const result = await response.json();

        if (result.success) {
            created.value = true;
            window.showToast('success', 'Created',
                'Record created successfully');
        } else {
            if (result.errors) {
                for (const key in result.errors) {
                    errors[key] = result.errors[key][0];
                }
            }
            errorMessage.value = result.message || 'Failed to create';
            window.showToast('error', 'Failed', result.message);
        }
    } catch (err) {
        errorMessage.value = 'An error occurred while submitting the form';
    } finally {
        submitting.value = false;
    }
}
Example 3: Loading States Pattern

Every page includes these essential reactive states for a polished UX:

loading-pattern.js
// Essential reactive states
const contentReady   = ref(false);  // Controls v-if on main content
const loading       = ref(true);    // Used for spinner display
const errorMessage  = ref(null);  // Error notification
const successMessage = ref(null);  // Success notification

// Auto-hide after a few seconds
setTimeout(() => { successMessage.value = null; }, 3000);
setTimeout(() => { errorMessage.value   = null; }, 4000);
Example 4: Custom Confirmation Modal for Delete

Delete operations use a custom modal (not confirm()) with smooth UX:

confirm-delete.js
const showDeleteModal = ref(false);
const deleting        = ref(false);

function closeDeleteModal() {
    showDeleteModal.value = false;
}

async function confirmDelete(id) {
    deleting.value = true;
    await new Promise(r => setTimeout(r, 500));  // Smooth delay

    try {
        const res = await fetch('/api/tax/' + id, { method: 'DELETE' });
        if (res.ok) {
            showDeleteModal.value = false;
            // Show success, reload table, redirect, etc.
        }
    } catch (err) {
        // Handle error
    } finally {
        deleting.value = false;
    }
}

// Toggle modal via v-bind:class / v-bind:style in HTML
// 
Vue Component Checklist

When creating a new Vue-enhanced page, ensure you include:

const { createApp, ref, reactive, onMounted } = Vue;
contentReady, loading, errorMessage states
500ms smooth delay before async operations
Loading spinner v-bind:disabled="submitting"
Success (3s) + Error (4s) auto-hide notifications
Custom modal for delete (not confirm())
Mount on #app-{action} (e.g., #app-create)
onMounted for initial data fetching

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 the development
  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? 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 18 Files

For every feature, the AI creates the full vertical slice:

{Entity}Controller.cs
4 CQRS Handlers + 2 Validators
{Entity}Endpoint.cs
4 Views (Index, Create, Edit, Detail)
4 JS Files (collocated with views)
Program.cs + DbContext updates
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 the development 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.0
Built with ASP.NET Core MVC 10 · Vue 3 · Hangfire · Serilog · EF Core · VSA Architecture