From bb33928661bc8ed50d43ee4ffc74baa5e8da85d6 Mon Sep 17 00:00:00 2001 From: "Kalana B. Thilakarathna" Date: Tue, 25 Aug 2026 16:11:37 +0530 Subject: [PATCH 1/2] auth clean up --- Distribution/Features/Auth/AuthController.cs | 21 +- Distribution/Features/Auth/AuthDTOs.cs | 6 +- Distribution/Features/Auth/AuthService.cs | 26 +- .../Features/Auth/AuthServiceClient.cs | 20 + .../Features/Auth/AuthServiceOptions.cs | 11 + .../Features/Auth/IAuthServiceClient.cs | 16 + Distribution/Program.cs | 71 +- Distribution/appsettings.json | 3 + sharedDocs/BACKEND_API_REFERENCE.md | 2601 +++++++++++++++++ 9 files changed, 2724 insertions(+), 51 deletions(-) create mode 100644 Distribution/Features/Auth/AuthServiceClient.cs create mode 100644 Distribution/Features/Auth/AuthServiceOptions.cs create mode 100644 Distribution/Features/Auth/IAuthServiceClient.cs create mode 100644 sharedDocs/BACKEND_API_REFERENCE.md diff --git a/Distribution/Features/Auth/AuthController.cs b/Distribution/Features/Auth/AuthController.cs index 1209f46..9f7504b 100644 --- a/Distribution/Features/Auth/AuthController.cs +++ b/Distribution/Features/Auth/AuthController.cs @@ -2,14 +2,27 @@ using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Auth; +/// +/// Proxies all authentication traffic from the Next.js frontend to the +/// external Auth Service. The frontend must never call the Auth Service +/// directly — all auth goes through this controller. +/// +/// TODO: Implement endpoints once Auth Service contract is agreed. +/// [ApiController] [Route("api/auth")] -public class AuthController(AuthService service) : ControllerBase +public class AuthController(IAuthServiceClient authServiceClient) : ControllerBase { + // TODO: replace stub endpoints below with real proxy calls once + // the Auth Service API contract is finalised. + [HttpPost("login")] - public async Task Login(LoginRequest request) + public Task Login() { - var result = await service.LoginAsync(request); - return result is null ? Unauthorized() : Ok(result); + // TODO: accept login request shape from Auth Service contract, + // forward to authServiceClient, relay response (token / session) back. + return Task.FromResult(StatusCode(501, new { error = "Auth integration not yet implemented." })); } + + // TODO: add further endpoints as needed (refresh, logout, etc.) } diff --git a/Distribution/Features/Auth/AuthDTOs.cs b/Distribution/Features/Auth/AuthDTOs.cs index bb7d71f..7d124b2 100644 --- a/Distribution/Features/Auth/AuthDTOs.cs +++ b/Distribution/Features/Auth/AuthDTOs.cs @@ -1,5 +1,5 @@ namespace Distribution.Features.Auth; -public record LoginRequest(string Username, string Password); - -public record LoginResponse(string Token, Guid UserId, string Username, string RoleName); +// TODO: Define request/response shapes once the Auth Service contract is agreed. +// These will be the DTOs passed between the Next.js frontend and this proxy controller, +// which should mirror (or adapt) whatever the Auth Service expects/returns. diff --git a/Distribution/Features/Auth/AuthService.cs b/Distribution/Features/Auth/AuthService.cs index e9f4928..ed74e7f 100644 --- a/Distribution/Features/Auth/AuthService.cs +++ b/Distribution/Features/Auth/AuthService.cs @@ -1,24 +1,6 @@ -using Distribution.Features.User; -using Distribution.Infra.Authorization; -using Microsoft.AspNetCore.Identity; - namespace Distribution.Features.Auth; -public class AuthService(IUserRepository userRepo, IJwtTokenService tokenService) -{ - private readonly PasswordHasher _hasher = new(); - - public async Task LoginAsync(LoginRequest request) - { - var user = await userRepo.GetByUsernameAsync(request.Username); - if (user is null || !user.IsActive) - return null; - - var result = _hasher.VerifyHashedPassword(user, user.PasswordHash, request.Password); - if (result == PasswordVerificationResult.Failed) - return null; - - var token = tokenService.GenerateToken(user); - return new LoginResponse(token, user.Id, user.Username, user.Role?.Name ?? string.Empty); - } -} +// TODO: This service previously contained local password validation logic. +// It has been replaced by the IAuthServiceClient connector which proxies +// to the external Auth Service. Remove this file or repurpose it for any +// local orchestration logic once the Auth Service contract is defined. diff --git a/Distribution/Features/Auth/AuthServiceClient.cs b/Distribution/Features/Auth/AuthServiceClient.cs new file mode 100644 index 0000000..2953534 --- /dev/null +++ b/Distribution/Features/Auth/AuthServiceClient.cs @@ -0,0 +1,20 @@ +namespace Distribution.Features.Auth; + +/// +/// Typed HttpClient that proxies requests to the external Auth Service. +/// Registered via AddHttpClient<AuthServiceClient> in Program.cs. +/// BaseAddress is configured from AuthService:BaseUrl in appsettings.json. +/// +/// TODO: Implement methods once Auth Service contract is agreed. +/// +public class AuthServiceClient(HttpClient http) : IAuthServiceClient +{ + // TODO: implement proxy methods, e.g.: + // + // public async Task LoginAsync(LoginRequest request, CancellationToken ct) + // { + // var response = await http.PostAsJsonAsync("/TODO_AUTH_SERVICE_LOGIN_PATH", request, ct); + // if (!response.IsSuccessStatusCode) return null; + // return await response.Content.ReadFromJsonAsync(ct); + // } +} diff --git a/Distribution/Features/Auth/AuthServiceOptions.cs b/Distribution/Features/Auth/AuthServiceOptions.cs new file mode 100644 index 0000000..5407a59 --- /dev/null +++ b/Distribution/Features/Auth/AuthServiceOptions.cs @@ -0,0 +1,11 @@ +namespace Distribution.Features.Auth; + +public class AuthServiceOptions +{ + public const string SectionName = "AuthService"; + + /// Base URL of the external Auth Service, e.g. "https://auth.internal". + public string BaseUrl { get; set; } = string.Empty; + + // TODO: add other config (API keys, timeout, etc.) once Auth Service is defined. +} diff --git a/Distribution/Features/Auth/IAuthServiceClient.cs b/Distribution/Features/Auth/IAuthServiceClient.cs new file mode 100644 index 0000000..a92a22a --- /dev/null +++ b/Distribution/Features/Auth/IAuthServiceClient.cs @@ -0,0 +1,16 @@ +namespace Distribution.Features.Auth; + +/// +/// Connector to the external Auth Service. This backend proxies all auth +/// traffic from the Next.js frontend through to the Auth Service — the +/// frontend never calls the Auth Service directly. +/// +/// TODO: Fill in the method signatures once the Auth Service contract is agreed. +/// +public interface IAuthServiceClient +{ + // TODO: define methods based on Auth Service contract, e.g.: + // Task LoginAsync(LoginRequest request, CancellationToken ct); + // Task RefreshAsync(string refreshToken, CancellationToken ct); + // Task LogoutAsync(string token, CancellationToken ct); +} diff --git a/Distribution/Program.cs b/Distribution/Program.cs index a2f48a2..c6b6803 100644 --- a/Distribution/Program.cs +++ b/Distribution/Program.cs @@ -91,7 +91,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -117,7 +116,6 @@ builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -125,28 +123,57 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); -var jwtSection = builder.Configuration.GetSection("Jwt"); -var jwtKey = jwtSection["Key"] ?? throw new InvalidOperationException("Jwt:Key is not configured."); +// ── Auth Service connector ─────────────────────────────────────────────────── +// TODO: replace placeholder BaseAddress with real Auth Service URL once decided. +// The AuthServiceOptions:BaseUrl value in appsettings.json drives this. +builder.Services.Configure( + builder.Configuration.GetSection(AuthServiceOptions.SectionName)); -builder.Services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.TokenValidationParameters = new TokenValidationParameters +var authServiceBaseUrl = builder.Configuration[$"{AuthServiceOptions.SectionName}:BaseUrl"]; + +builder.Services.AddHttpClient(client => +{ + // TODO: set BaseAddress once Auth Service URL is known. + if (!string.IsNullOrWhiteSpace(authServiceBaseUrl)) + client.BaseAddress = new Uri(authServiceBaseUrl); +}); + +builder.Services.AddScoped(); + +// ── Authentication ─────────────────────────────────────────────────────────── +// TODO: replace this JWT configuration with the token validation strategy +// dictated by the Auth Service (symmetric key, JWKS/OIDC, etc.) once decided. +var jwtSection = builder.Configuration.GetSection("Jwt"); +var jwtKey = jwtSection["Key"]; + +if (!string.IsNullOrWhiteSpace(jwtKey)) +{ + builder.Services.AddAuthentication(options => { - ValidateIssuer = true, - ValidIssuer = jwtSection["Issuer"], - ValidateAudience = true, - ValidAudience = jwtSection["Audience"], - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)), - ValidateLifetime = true, - ClockSkew = TimeSpan.FromMinutes(1) - }; - }); + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtSection["Issuer"], + ValidateAudience = true, + ValidAudience = jwtSection["Audience"], + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)), + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(1) + }; + }); +} +else +{ + // Auth Service integration not yet configured — all [Authorize] checks will + // fail until the token validation strategy is wired up. + builder.Services.AddAuthentication(); +} builder.Services.AddAuthorization(); diff --git a/Distribution/appsettings.json b/Distribution/appsettings.json index e217f92..0f035e0 100644 --- a/Distribution/appsettings.json +++ b/Distribution/appsettings.json @@ -17,5 +17,8 @@ }, "StoreManagement": { "UnverifiedKycOrderThreshold": 0 + }, + "AuthService": { + "BaseUrl": "" } } diff --git a/sharedDocs/BACKEND_API_REFERENCE.md b/sharedDocs/BACKEND_API_REFERENCE.md new file mode 100644 index 0000000..31a58a4 --- /dev/null +++ b/sharedDocs/BACKEND_API_REFERENCE.md @@ -0,0 +1,2601 @@ +# Distribution ERP — Backend API Reference + +> **Stack:** ASP.NET Core (.NET 9) · MySQL (Entity Framework Core) +> **Base URL:** `https://` (configure per environment) +> **Swagger UI:** available at `/swagger` in Development mode +> **Content-Type:** `application/json` for all requests unless noted (`multipart/form-data` for KYC uploads) + +--- + +## Table of Contents + +1. [Authentication & JWT](#1-authentication--jwt) +2. [Authorization Model](#2-authorization-model) +3. [Global Response Conventions](#3-global-response-conventions) +4. [Enumerations Reference](#4-enumerations-reference) +5. [Pagination](#5-pagination) +6. [Modules](#6-modules) + - [Auth](#61-auth) + - [Users](#62-users) + - [Roles](#63-roles) + - [Authorization Checks](#64-authorization-checks) + - [Regions](#65-regions) + - [Routes](#66-routes) + - [Vehicles](#67-vehicles) + - [Warehouses](#68-warehouses) + - [Stores](#69-stores) + - [Store KYC Documents](#610-store-kyc-documents) + - [Categories & Subcategories](#611-categories--subcategories) + - [Brands](#612-brands) + - [Units of Measure (UOMs)](#613-units-of-measure-uoms) + - [Items](#614-items) + - [Price Lists](#615-price-lists) + - [Schemes (Promotions)](#616-schemes-promotions) + - [Suppliers](#617-suppliers) + - [Purchase Orders (PO)](#618-purchase-orders-po) + - [Goods Receipt Notes (GRN)](#619-goods-receipt-notes-grn) + - [Purchase Returns](#620-purchase-returns) + - [Stock](#621-stock) + - [Driver Profiles](#622-driver-profiles) + - [Driver Compliance Config](#623-driver-compliance-config) + - [Rides](#624-rides) + - [Loading Sheets](#625-loading-sheets) + - [Delivery Tasks](#626-delivery-tasks) + - [Sales](#627-sales) + - [Orders](#628-orders) + - [Payments](#629-payments) + - [Sync (ERP Import)](#630-sync-erp-import) +7. [Business Flows](#7-business-flows) + - [Region → Route → Store Setup](#71-region--route--store-setup) + - [Sale Flow](#72-sale-flow-sales-run) + - [Delivery Flow](#73-delivery-flow-end-to-end-ride-lifecycle) + - [Ride Cancellation Flow](#74-ride-cancellation-flow) + - [Payment Flow](#75-payment-flow) + - [Purchase Order Flow](#76-purchase-order-flow-procurement) + - [Loading Sheet Flow](#77-loading-sheet-flow-detail) + - [Stock Flow](#78-stock-flow-how-stock-moves) + - [Scheme / Promotion Flow](#79-scheme--promotion-flow) + +--- + +## 1. Authentication + +> ⚠️ **YET TO IMPLEMENT** +> +> Authentication is handled by a **dedicated external Auth Service** (separate microservice). This backend acts as a **connector/proxy** — the Next.js frontend never calls the Auth Service directly; all auth traffic flows through this backend's `/api/auth/*` endpoints. +> +> The exact contract (token format, claims, endpoints, session strategy) is not yet finalized. The `/api/auth` routes are scaffolded in the backend as pass-through stubs pending Auth Service integration. +> +> What is decided: +> - The frontend calls **this backend** for all auth operations, not the Auth Service directly. +> - This backend forwards the request to the Auth Service and relays the response. +> - All other endpoints on this backend will validate whatever credential the Auth Service issues (token format TBD). +> +> **Do not implement auth in the frontend until this section is updated.** + +--- + +## 2. Authorization Model + +The system uses two orthogonal authorization concepts: + +### 2.1 Permission-Based Access Control + +Every protected endpoint is guarded by a **policy string** (e.g. `"orders.view"`). The JWT's `role_id` claim is used to look up the role's granted permissions in the database. The frontend should gracefully hide/disable UI elements the user lacks permission for (use the `/api/authorization/permissions/{code}` endpoint to check at runtime). + +#### Full Permission Code List + +| Permission Code | What it Controls | +|---|---| +| `items.view` | Read items, brands, UOMs | +| `items.create` | Create items, brands, UOMs; also gate for Sync endpoints | +| `items.update` | Update items, brands, UOMs | +| `items.delete` | Delete items, brands, UOMs | +| `categories.view` | Read categories/subcategories | +| `categories.create` | Create categories/subcategories | +| `categories.update` | Update categories/subcategories | +| `categories.delete` | Delete categories/subcategories | +| `regions.view` | Read regions | +| `regions.create` | Create regions | +| `regions.update` | Update regions | +| `regions.delete` | Delete regions | +| `routes.view` | Read routes | +| `routes.create` | Create routes | +| `routes.update` | Update routes | +| `routes.delete` | Delete routes | +| `vehicles.view` | Read vehicles | +| `vehicles.create` | Create vehicles | +| `vehicles.update` | Update vehicles | +| `vehicles.delete` | Delete vehicles | +| `warehouses.view` | Read warehouses | +| `warehouses.create` | Create warehouses | +| `warehouses.update` | Update warehouses | +| `warehouses.delete` | Delete warehouses | +| `stores.view` | Read stores, visit history, dashboard | +| `stores.create` | Create stores | +| `stores.update` | Update stores, upload KYC docs | +| `stores.delete` | Delete stores | +| `stores.credit.update` | Update credit limit / outstanding balance | +| `stores.kyc.verify` | Change a store's KYC status | +| `orders.view` | Read orders, sales | +| `orders.create` | Create orders, sales | +| `orders.update` | Update orders | +| `orders.delete` | Delete orders | +| `users.view` | Read users | +| `users.create` | Create users | +| `users.update` | Update users | +| `users.assign_role` | Change a user's role | +| `users.assign_scope` | Set a user's region/route/store scope | +| `users.deactivate` | Deactivate users | +| `driver_profiles.view` | Read driver profiles | +| `driver_profiles.create` | Create driver profiles | +| `driver_profiles.update` | Update driver profiles | +| `driver_profiles.deactivate` | Deactivate driver profiles | +| `compliance.manage` | Read/update driver compliance config | +| `rides.view` | Read rides, cancel requests | +| `rides.create` | Create rides | +| `rides.manage` | Assign driver, open, activate, close rides | +| `rides.cancel.request` | Raise a cancel request on a ride | +| `rides.cancel.approve` | Approve or reject a cancel request | +| `loading.view` | Read loading sheets | +| `loading.dispatch` | Create loading sheets, set items, confirm dispatch | +| `loading.receive` | Set receipt items, confirm receipt | +| `loading.resolve` | Resolve discrepancy on a loading sheet | + +### 2.2 Scope-Based Data Filtering + +Users can be restricted to specific **regions**, **routes**, or **stores**. Certain read operations (orders/sales by store, etc.) enforce this scope. Use the scope-check endpoints to determine what data a user can access: + +- `GET /api/authorization/scopes/stores/{storeId}` — can the current user access this store? +- `GET /api/authorization/scopes/routes/{routeId}` — can the current user access this route? +- `GET /api/authorization/scopes/regions/{regionId}` — can the current user access this region? +- `GET /api/authorization/scopes/stores` — list all store IDs the current user can access + +--- + +## 3. Global Response Conventions + +| HTTP Status | Meaning | +|---|---| +| `200 OK` | Success (read / update / action) | +| `201 Created` | Resource created; `Location` header + body returned | +| `204 No Content` | Delete successful | +| `400 Bad Request` | Validation error or invalid state; body: `{ "error": "message" }` | +| `401 Unauthorized` | Missing or invalid JWT | +| `403 Forbidden` | Authenticated but access denied (scope) | +| `404 Not Found` | Resource not found | +| `409 Conflict` | Business rule violation (duplicate, already exists, etc.) | + +All timestamps are **ISO 8601 UTC** strings (e.g. `"2024-01-15T09:30:00Z"`). +All IDs are **GUIDs** in the format `"3fa85f64-5717-4562-b3fc-2c963f66afa6"`. + +--- + +## 4. Enumerations Reference + +### `KycStatus` +| Value | Description | +|---|---| +| `0` / `"Pending"` | Awaiting review | +| `1` / `"Verified"` | KYC approved | +| `2` / `"Rejected"` | KYC rejected | + +### `LocationType` +| Value | Description | +|---|---| +| `0` / `"Warehouse"` | Stock held in a warehouse | +| `1` / `"Vehicle"` | Stock loaded on a vehicle | + +### `MovementType` +| Value | Description | +|---|---| +| `0` / `"Sale"` | Stock reduced by a sale | +| `1` / `"CustomerReturn"` | Stock returned by customer | +| `2` / `"Transfer"` | Moved between locations | +| `3` / `"Damage"` | Written off as damaged | +| `4` / `"Adjustment"` | Manual stock correction | +| `5` / `"Load"` | Vehicle loading event | +| `6` / `"PurchaseReceipt"` | GRN stock addition | +| `7` / `"PurchaseReturn"` | Returned to supplier | + +### `LoadingSheetStatus` +| Value | Description | +|---|---| +| `0` / `"Pending"` | Created, not yet dispatched | +| `1` / `"DispatchConfirmed"` | Warehouse confirmed dispatch | +| `2` / `"ReceiptConfirmed"` | Driver confirmed receipt | +| `3` / `"DiscrepancyFlagged"` | Quantities differ; needs resolution | +| `4` / `"Resolved"` | Discrepancy settled | + +### `RideType` +| Value | Description | +|---|---| +| `0` / `"Delivery"` | Delivery-only run | +| `1` / `"Sale"` | Sales run (driver can take orders) | + +### `RideStatus` +| Value | Description | +|---|---| +| `0` / `"Draft"` | Created, not yet started | +| `1` / `"Open"` | Route opened, ready to activate | +| `2` / `"Active"` | Currently in progress | +| `3` / `"CancelRequested"` | Cancel requested, pending approval | +| `4` / `"Cancelled"` | Cancelled | +| `5` / `"Closed"` | Completed | + +### `CancelRequestType` +| Value | Description | +|---|---| +| `0` / `"VehicleBreakdown"` | Vehicle broke down | +| `1` / `"DriverUnavailable"` | Driver unavailable | +| `2` / `"RouteIssue"` | Route-related issue | +| `3` / `"Other"` | Other reason | + +### `CancelRequestStatus` +| Value | Description | +|---|---| +| `0` / `"Pending"` | Awaiting approval decision | +| `1` / `"Approved"` | Cancel request approved | +| `2` / `"Rejected"` | Cancel request rejected | + +### `PurchaseOrderStatus` +| Value | Description | +|---|---| +| `0` / `"Issued"` | PO created, not yet received | +| `1` / `"PartiallyReceived"` | Some items received via GRN | +| `2` / `"Received"` | All items received | +| `3` / `"Closed"` | Manually closed | +| `4` / `"Cancelled"` | PO cancelled | + +### `SaleStatus` +| Value | Description | +|---|---| +| `0` / `"Pending"` | Sale not yet completed | +| `1` / `"Completed"` | Sale fulfilled | +| `2` / `"Cancelled"` | Sale cancelled | + +### `OrderStatus` +| Value | Description | +|---|---| +| `0` / `"Pending"` | Order awaiting fulfilment | +| `1` / `"PartiallyFulfilled"` | Some items fulfilled | +| `2` / `"Fulfilled"` | Fully fulfilled | +| `3` / `"Cancelled"` | Order cancelled | + +### `PaymentStatus` +| Value | Description | +|---|---| +| `0` / `"Pending"` | No payment received | +| `1` / `"PartiallyPaid"` | Partial payment received | +| `2` / `"Paid"` | Fully paid | + +### `PaymentMethod` +| Value | Description | +|---|---| +| `0` / `"Cash"` | Cash payment | +| `1` / `"Credit"` | Credit payment | + +### `DeliveryTaskStatus` +| Value | Description | +|---|---| +| `0` / `"Pending"` | Not yet actioned | +| `1` / `"Delivered"` | Fully delivered | +| `2` / `"PartiallyDelivered"` | Partially delivered | +| `3` / `"Failed"` | Delivery failed | + +### `VehicleType` +| Value | Description | +|---|---| +| `0` / `"Car"` | Car | +| `1` / `"Truck"` | Truck | +| `2` / `"Van"` | Van | +| `3` / `"Motorcycle"` | Motorcycle | +| `4` / `"Bicycle"` | Bicycle | + +### `WareHouseType` +| Value | Description | +|---|---| +| `0` / `"DistributionCenter"` | Distribution center | +| `1` / `"FulfillmentCenter"` | Fulfilment center | +| `2` / `"CrossDocking"` | Cross-docking facility | +| `3` / `"ColdStorage"` | Cold storage | +| `4` / `"BondedWarehouse"` | Bonded warehouse | +| `5` / `"AutomatedWarehouse"` | Automated warehouse | + +### `SchemeRuleType` +| Value | Description | +|---|---| +| `0` / `"BuyXGetY"` | Buy X quantity of trigger item, get Y free | +| `1` / `"SlabDiscount"` | Tiered order-value discount | +| `2` / `"FlatPercent"` | Flat percentage discount | +| `3` / `"FreeBundle"` | Buy a bundle of required items, get free item | + +### `SyncSource` (on Items) +| Value | Description | +|---|---| +| `0` / `"Manual"` | Created directly in this system | +| `1` / `"Erp"` | Imported via the Sync API | + +--- + +## 5. Pagination + +Endpoints that support pagination accept these query parameters: + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `page` | `int` | `1` | 1-based page number | +| `pageSize` | `int` | `20` | Records per page | + +Paginated responses use this wrapper: +```json +{ + "page": 1, + "pageSize": 20, + "total": 150, + "items": [ ... ] +} +``` + +**Endpoints with pagination:** Driver Profiles, Rides, Loading Sheets, Stock Balances, Stock Movements, Ride Cancel Requests. + +All other list endpoints return a plain array — **implement client-side pagination or virtual scrolling for large datasets**. + +--- + +## 6. Modules + +--- + +### 6.1 Auth + +> ⚠️ **YET TO IMPLEMENT** — See [Section 1](#1-authentication) for context. +> +> The `/api/auth` routes are stubs. Shape of requests and responses will be defined once the Auth Service contract is agreed. + +**Base path:** `/api/auth` + +--- + +### 6.2 Users + +**Base path:** `/api/users` +**Required:** JWT token + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/users` | `users.view` | List all users | +| `GET` | `/api/users/{id}` | `users.view` | Get user by ID | +| `POST` | `/api/users` | `users.create` | Create user | +| `PUT` | `/api/users/{id}` | `users.update` | Update user full name | +| `PUT` | `/api/users/{id}/role` | `users.assign_role` | Assign role to user | +| `PUT` | `/api/users/{id}/scope` | `users.assign_scope` | Set user's data scope | +| `DELETE` | `/api/users/{id}` | `users.deactivate` | Deactivate user | + +#### Create User — `POST /api/users` +```json +{ + "username": "string", // required, unique login name + "password": "string", // required, plaintext (hashed server-side) + "fullName": "string", // required + "roleId": "guid", // required + "reportsToUserId": "guid" // optional, parent user in hierarchy +} +``` + +#### Update User — `PUT /api/users/{id}` +```json +{ "fullName": "string" } +``` + +#### Assign Role — `PUT /api/users/{id}/role` +```json +{ "roleId": "guid" } +``` + +#### Assign Scope — `PUT /api/users/{id}/scope` +```json +{ + "regionIds": ["guid", ...], // list (can be empty) + "routeIds": ["guid", ...], + "storeIds": ["guid", ...] +} +``` + +#### User Response Object +```json +{ + "id": "guid", + "username": "string", + "fullName": "string", + "roleId": "guid", + "roleName": "string", + "reportsToUserId": "guid | null", + "isActive": true, + "createdAt": "datetime", + "updatedAt": "datetime | null" +} +``` + +--- + +### 6.3 Roles + +**Base path:** `/api/roles` +**Required:** JWT token (no specific permission — any authenticated user) + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/roles` | List all roles | +| `GET` | `/api/roles/{id}` | Get role by ID | +| `GET` | `/api/roles/{id}/permissions` | Get permissions assigned to role | + +#### Role Response +```json +{ + "id": "guid", + "name": "string", + "level": 1, // integer hierarchy level; lower = more authority + "isActive": true, + "createdAt": "datetime" +} +``` + +#### Role Permissions Response +Array of: +```json +{ + "permissionId": "guid", + "code": "orders.view", // use this code for Policy checks + "description": "string" +} +``` + +--- + +### 6.4 Authorization Checks + +**Base path:** `/api/authorization` +**Required:** JWT token + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/authorization/permissions/{permissionCode}` | Check if current user's role has a permission | +| `GET` | `/api/authorization/scopes/stores/{storeId}` | Check if current user can access a store | +| `GET` | `/api/authorization/scopes/routes/{routeId}` | Check if current user can access a route | +| `GET` | `/api/authorization/scopes/regions/{regionId}` | Check if current user can access a region | +| `GET` | `/api/authorization/scopes/stores` | Get all store IDs accessible to current user | + +**Permission check response:** +```json +{ "hasPermission": true } +``` + +**Scope check responses:** +```json +{ "hasScope": true } // stores/routes/regions +{ "storeIds": ["guid", "guid", ...] } // accessible stores list +``` + +--- + +### 6.5 Regions + +**Base path:** `/api/regions` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/regions` | `regions.view` | List all regions | +| `GET` | `/api/regions/{id}` | `regions.view` | Get region by ID | +| `POST` | `/api/regions` | `regions.create` | Create region | +| `PUT` | `/api/regions/{id}` | `regions.update` | Update region | +| `DELETE` | `/api/regions/{id}` | `regions.delete` | Delete region | + +**Create/Update request:** +```json +{ "name": "string" } +``` + +**Response:** +```json +{ + "id": "guid", + "name": "string", + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.6 Routes + +**Base path:** `/api/routes` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/routes` | `routes.view` | List all routes | +| `GET` | `/api/routes/{id}` | `routes.view` | Get route by ID | +| `POST` | `/api/routes` | `routes.create` | Create route | +| `PUT` | `/api/routes/{id}` | `routes.update` | Update route | +| `DELETE` | `/api/routes/{id}` | `routes.delete` | Delete route | + +**Create/Update request:** +```json +{ + "regionId": "guid", // required + "routeName": "string", // required + "startPoint": "string", // required + "endPoint": "string", // required + "length": 12.5 // decimal (kilometers) +} +``` + +**Response:** +```json +{ + "id": "guid", + "regionId": "guid", + "routeName": "string", + "startPoint": "string", + "endPoint": "string", + "length": 12.5, + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.7 Vehicles + +**Base path:** `/api/vehicles` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/vehicles` | `vehicles.view` | List all vehicles | +| `GET` | `/api/vehicles/{id}` | `vehicles.view` | Get vehicle by ID | +| `POST` | `/api/vehicles` | `vehicles.create` | Create vehicle | +| `PUT` | `/api/vehicles/{id}` | `vehicles.update` | Update vehicle | +| `DELETE` | `/api/vehicles/{id}` | `vehicles.delete` | Delete vehicle | + +**Create/Update request:** +```json +{ + "regNumber": "string", // registration number + "numberPlate": "string", // display plate + "type": "Van" // VehicleType enum +} +``` + +**Response:** +```json +{ + "id": "guid", + "regNumber": "string", + "numberPlate": "string", + "type": "Van", + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.8 Warehouses + +**Base path:** `/api/warehouses` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/warehouses` | `warehouses.view` | List all warehouses | +| `GET` | `/api/warehouses/{id}` | `warehouses.view` | Get warehouse by ID | +| `POST` | `/api/warehouses` | `warehouses.create` | Create warehouse | +| `PUT` | `/api/warehouses/{id}` | `warehouses.update` | Update warehouse | +| `DELETE` | `/api/warehouses/{id}` | `warehouses.delete` | Delete warehouse | + +**Create/Update request:** +```json +{ + "name": "string", + "location": "string", + "type": "DistributionCenter", // WareHouseType enum + "vehicleId": "guid | null" // optional, link to a vehicle +} +``` + +**Response:** +```json +{ + "id": "guid", + "name": "string", + "location": "string", + "type": "DistributionCenter", + "vehicleId": "guid | null", + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.9 Stores + +**Base path:** `/api/stores` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/stores` | `stores.view` | List all stores | +| `GET` | `/api/stores/{id}` | `stores.view` | Get store by ID | +| `POST` | `/api/stores` | `stores.create` | Create store | +| `PUT` | `/api/stores/{id}` | `stores.update` | Update store | +| `DELETE` | `/api/stores/{id}` | `stores.delete` | Delete store | +| `PUT` | `/api/stores/{id}/credit-limit` | `stores.credit.update` | Update credit limit | +| `PUT` | `/api/stores/{id}/outstanding-balance` | `stores.credit.update` | Update outstanding balance | +| `GET` | `/api/stores/{id}/outstanding-balance` | `stores.view` | Get outstanding balance details | +| `PUT` | `/api/stores/{id}/kyc/verify` | `stores.kyc.verify` | Verify or reject KYC | +| `PUT` | `/api/stores/{id}/segment` | `stores.update` | Classify store segment | +| `GET` | `/api/stores/{id}/visit-history` | `stores.view` | Get store visit history | +| `GET` | `/api/stores/{id}/schemes/{schemeId}/eligibility` | `stores.view` | Check scheme eligibility | +| `GET` | `/api/stores/{id}/dashboard` | `stores.view` | Get store dashboard metrics | + +#### Create/Update Store +```json +{ + "routeId": "guid", + "ownerName": "string", + "storeName": "string", + "storeAddress": "string", + "latitude": 6.927079, + "longitude": 79.861244, + "oneTimeAmount": 0.00, // one-time registration/setup fee + "creditAmount": 0.00 // assigned credit amount +} +``` + +#### Store Response +```json +{ + "id": "guid", + "routeId": "guid", + "ownerName": "string", + "storeName": "string", + "storeAddress": "string", + "latitude": 6.927079, + "longitude": 79.861244, + "oneTimeAmount": 0.00, + "creditAmount": 0.00, + "creditLimit": 50000.00, + "outstandingBalance": 12500.00, + "segment": "Gold | null", // Gold, Silver, Key Account, High Risk, etc. + "kycStatus": "Pending", + "isActive": true, + "createdAt": "datetime" +} +``` + +#### Update Credit Limit — `PUT /api/stores/{id}/credit-limit` +```json +{ + "newLimit": 75000.00, + "approverId": "guid" // user ID of the approver +} +``` + +#### Update Outstanding Balance — `PUT /api/stores/{id}/outstanding-balance` +```json +{ "newBalance": 10000.00 } +``` + +#### Outstanding Balance Response — `GET /api/stores/{id}/outstanding-balance` +```json +{ + "storeId": "guid", + "creditLimit": 50000.00, + "outstandingBalance": 12500.00, + "availableCredit": 37500.00 +} +``` + +#### Verify KYC — `PUT /api/stores/{id}/kyc/verify` +```json +{ + "status": "Verified", // KycStatus: Verified or Rejected + "notes": "string | null" +} +``` + +#### Classify Segment — `PUT /api/stores/{id}/segment` +```json +{ "segment": "Gold" } +``` + +#### Visit History Response — `GET /api/stores/{id}/visit-history` +Array of: +```json +{ + "visitId": "guid", + "visitDate": "datetime", + "repUserId": "guid", + "outcome": "string" +} +``` + +#### Scheme Eligibility — `GET /api/stores/{id}/schemes/{schemeId}/eligibility` +```json +{ + "eligible": true, + "reason": "string | null" +} +``` + +#### Store Dashboard — `GET /api/stores/{id}/dashboard?period=monthly` +`period` query param is a string (e.g. `"daily"`, `"weekly"`, `"monthly"`). + +```json +{ + "storeId": "guid", + "period": "monthly", + "orderCount": 12, + "totalOrderValue": 145000.00, + "lastOrderDate": "datetime | null" +} +``` + +--- + +### 6.10 Store KYC Documents + +**Base path:** `/api/stores/{storeId}/kyc-documents` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/stores/{storeId}/kyc-documents` | `stores.view` | Get all KYC documents for a store | +| `POST` | `/api/stores/{storeId}/kyc-documents` | `stores.update` | Upload a KYC document | + +#### Upload KYC Document — `POST /api/stores/{storeId}/kyc-documents` + +> **Content-Type:** `multipart/form-data` + +| Form field | Type | Required | Description | +|---|---|---|---| +| `documentType` | `string` | Yes | Document type label (e.g. `"NIC"`, `"Business Registration"`) | +| `file` | `File` | Yes | The document file | + +**Response:** +```json +{ + "id": "guid", + "storeId": "guid", + "documentType": "NIC", + "fileReference": "string", // server-side file reference/path + "uploadedAt": "datetime" +} +``` + +--- + +### 6.11 Categories & Subcategories + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/categories` | `categories.view` | List all categories (with subcategories) | +| `GET` | `/api/categories/{id}` | `categories.view` | Get category by ID | +| `POST` | `/api/categories` | `categories.create` | Create category | +| `PUT` | `/api/categories/{id}` | `categories.update` | Update category | +| `DELETE` | `/api/categories/{id}` | `categories.delete` | Delete category | +| `POST` | `/api/categories/{categoryId}/subcategories` | `categories.create` | Create subcategory | +| `PUT` | `/api/subcategories/{id}` | `categories.update` | Update subcategory | +| `DELETE` | `/api/subcategories/{id}` | `categories.delete` | Delete subcategory | + +**Create/Update category or subcategory:** +```json +{ "name": "string" } +``` + +**Category detail response (GET by ID):** +```json +{ + "id": "guid", + "name": "string", + "isActive": true, + "createdAt": "datetime", + "subCategories": [ + { "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime" } + ] +} +``` + +**Category list response (GET all):** Array of above objects. + +--- + +### 6.12 Brands + +**Base path:** `/api/brands` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/brands` | `items.view` | List all brands | +| `GET` | `/api/brands/{id}` | `items.view` | Get brand by ID | +| `POST` | `/api/brands` | `items.create` | Create brand | +| `PUT` | `/api/brands/{id}` | `items.update` | Update brand | +| `DELETE` | `/api/brands/{id}` | `items.delete` | Delete brand | + +**Create/Update:** `{ "name": "string" }` + +**Response:** `{ "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime" }` + +--- + +### 6.13 Units of Measure (UOMs) + +**Base path:** `/api/uoms` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/uoms` | `items.view` | List all UOMs | +| `GET` | `/api/uoms/{id}` | `items.view` | Get UOM by ID | +| `POST` | `/api/uoms` | `items.create` | Create UOM | +| `PUT` | `/api/uoms/{id}` | `items.update` | Update UOM | +| `DELETE` | `/api/uoms/{id}` | `items.delete` | Delete UOM | + +**Create/Update:** `{ "name": "string" }` + +**Response:** `{ "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime" }` + +--- + +### 6.14 Items + +**Base path:** `/api/items` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/items` | `items.view` | List items (filterable) | +| `GET` | `/api/items/{id}` | `items.view` | Get item detail | +| `POST` | `/api/items` | `items.create` | Create item | +| `PUT` | `/api/items/{id}` | `items.update` | Update item | +| `DELETE` | `/api/items/{id}` | `items.delete` | Delete item | +| `PUT` | `/api/items/{id}/uom-conversions` | `items.update` | Replace all UOM conversions | + +#### List Items — `GET /api/items` + +Optional query filters: +| Param | Type | Description | +|---|---|---| +| `categoryId` | `guid` | Filter by category | +| `subCategoryId` | `guid` | Filter by subcategory | +| `brandId` | `guid` | Filter by brand | + +#### Create Item — `POST /api/items` +```json +{ + "sku": "string", // required, unique SKU + "name": "string", // required + "description": "string", // optional + "categoryId": "guid", // required + "subCategoryId": "guid", // optional + "brandId": "guid", // optional + "baseUomId": "guid", // required, base unit of measure + "salePrice": 150.00, // optional default sale price + "conversions": [ // optional UOM conversion rules + { + "fromUomId": "guid", + "toUomId": "guid", + "factor": 12.0 // 1 fromUom = factor toUom + } + ] +} +``` +Returns `409 Conflict` if SKU already exists. + +#### Update Item — `PUT /api/items/{id}` +```json +{ + "name": "string", + "description": "string | null", + "categoryId": "guid", + "subCategoryId": "guid | null", + "brandId": "guid | null", + "baseUomId": "guid", + "salePrice": 150.00 +} +``` + +#### Replace UOM Conversions — `PUT /api/items/{id}/uom-conversions` +Replaces **all** existing conversions for the item. +```json +{ + "conversions": [ + { "fromUomId": "guid", "toUomId": "guid", "factor": 12.0 } + ] +} +``` + +#### Item Summary Response (list) +```json +{ + "id": "guid", + "sku": "string", + "name": "string", + "categoryId": "guid", + "categoryName": "string", + "subCategoryId": "guid | null", + "subCategoryName": "string | null", + "brandId": "guid | null", + "brandName": "string | null", + "baseUomId": "guid", + "baseUomName": "string", + "salePrice": 150.00, + "syncSource": "Manual | Erp", + "isActive": true, + "createdAt": "datetime" +} +``` + +#### Item Detail Response (single) +Same as summary plus: +```json +{ + "description": "string | null", + "conversions": [ + { + "id": "guid", + "fromUomId": "guid", + "fromUomName": "string", + "toUomId": "guid", + "toUomName": "string", + "factor": 12.0 + } + ] +} +``` + +--- + +### 6.15 Price Lists + +**Base path:** `/api/price-lists` +**Required:** JWT token (no specific permission policy — any authenticated user) + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/price-lists` | List price lists (filterable by region) | +| `GET` | `/api/price-lists/{id}` | Get price list with all items | +| `POST` | `/api/price-lists` | Create price list | +| `PUT` | `/api/price-lists/{id}` | Update price list metadata | +| `DELETE` | `/api/price-lists/{id}` | Delete price list | +| `PUT` | `/api/price-lists/{id}/items/{sku}` | Upsert an item's price in this list | +| `DELETE` | `/api/price-lists/{id}/items/{sku}` | Remove an item from this list | + +#### List Price Lists — `GET /api/price-lists` +Optional query: `?regionId={guid}` + +#### Create/Update Price List +```json +{ + "name": "string", + "regionId": "guid | null", // null = applies to all regions + "effectiveFrom": "datetime", + "effectiveTo": "datetime" +} +``` +Returns `409 Conflict` if overlapping active price list for the same region exists. + +#### Upsert Item Price — `PUT /api/price-lists/{id}/items/{sku}` +`sku` is the item's SKU string in the URL path. +```json +{ + "price": 150.00, + "minPrice": 130.00 // optional, null = price is non-negotiable +} +``` + +#### Price List Response +```json +{ + "id": "guid", + "name": "string", + "regionId": "guid | null", + "regionName": "string | null", + "effectiveFrom": "datetime", + "effectiveTo": "datetime", + "isActive": true, + "createdAt": "datetime", + "items": [ + { + "id": "guid", + "itemId": "guid", + "sku": "string", + "itemName": "string", + "price": 150.00, + "minPrice": 130.00 + } + ] +} +``` + +--- + +### 6.16 Schemes (Promotions) + +**Base path:** `/api/schemes` +**Required:** JWT token (no specific permission policy) + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/schemes` | List schemes (optional `?regionId={guid}`) | +| `GET` | `/api/schemes/{id}` | Get scheme detail | +| `POST` | `/api/schemes` | Create scheme | +| `PUT` | `/api/schemes/{id}` | Update scheme metadata | +| `DELETE` | `/api/schemes/{id}` | Delete scheme | +| `PUT` | `/api/schemes/{id}/free-goods-rule` | Set free-goods rule | +| `DELETE` | `/api/schemes/{id}/free-goods-rule` | Clear free-goods rule | +| `PUT` | `/api/schemes/{id}/bundle-components/{itemId}` | Upsert bundle component | +| `DELETE` | `/api/schemes/{id}/bundle-components/{itemId}` | Remove bundle component | +| `GET` | `/api/schemes/eligible?storeId={guid}&orderId={guid}` | Get eligible schemes for a store+order | +| `GET` | `/api/schemes/{id}/spend` | Get scheme spend analytics | +| `GET` | `/api/schemes/{id}/roi` | Get scheme ROI analytics | +| `POST` | `/api/orders/{id}/apply-scheme` | Apply a scheme to an order | + +#### Scheme Rule Types + +- **`BuyXGetY`**: Store buys `triggerQty` of `triggerItemId`, gets `freeQty` of `freeItemId` for free. +- **`SlabDiscount`**: Tiered discount based on order value. `ruleConfig` JSON: `{ "tiers": [ { "minOrderAmount": 10000, "discountPercent": 5.0 }, ... ] }` (ordered ascending). +- **`FlatPercent`**: Flat percentage off. `ruleConfig` JSON: `{ "discountPercent": 10.0 }`. +- **`FreeBundle`**: Store buys all required bundle components, gets `freeQty` of `freeItemId` free. + +#### Create Scheme — `POST /api/schemes` +```json +{ + "name": "string", + "ruleType": "SlabDiscount", // SchemeRuleType enum + "ruleConfig": "{...}", // JSON string; required for SlabDiscount/FlatPercent, null for BuyXGetY/FreeBundle + "regionId": "guid | null", // null = all regions + "eligibleSegment": "Gold | null", // null = all segments + "dateRangeFrom": "datetime", + "dateRangeTo": "datetime", + "agentLimitExempt": false, // if true, exempt from agent discount ceiling + "baselineExpectedSales": 500000.00, // optional, used for ROI calc + // Optional one-shot fields (can also use separate endpoints later): + "triggerItemId": "guid | null", // BuyXGetY: item store must buy + "triggerQty": 10, // BuyXGetY: quantity trigger + "freeItemId": "guid | null", // BuyXGetY/FreeBundle: item given free + "freeQty": 2.0, // quantity of free item + "bundleComponents": [ // FreeBundle: required purchase items + { "itemId": "guid", "qty": 5 } + ] +} +``` + +#### Update Scheme — `PUT /api/schemes/{id}` +```json +{ + "name": "string", + "ruleConfig": "{...} | null", + "regionId": "guid | null", + "eligibleSegment": "string | null", + "dateRangeFrom": "datetime", + "dateRangeTo": "datetime", + "agentLimitExempt": false, + "baselineExpectedSales": 500000.00 +} +``` + +#### Set Free Goods Rule — `PUT /api/schemes/{id}/free-goods-rule` +```json +{ + "triggerItemId": "guid | null", // required for BuyXGetY; ignored for FreeBundle + "triggerQty": 10, // required for BuyXGetY; ignored for FreeBundle + "freeItemId": "guid", // required + "freeQty": 2.0 // required +} +``` + +#### Upsert Bundle Component — `PUT /api/schemes/{id}/bundle-components/{itemId}` +```json +{ "qty": 5 } +``` + +#### Apply Scheme to Order — `POST /api/orders/{id}/apply-scheme` +```json +{ "schemeId": "guid" } +``` + +#### Scheme Response +```json +{ + "id": "guid", + "name": "string", + "ruleType": "BuyXGetY", + "ruleConfig": "string | null", + "triggerItemId": "guid | null", + "triggerItemName": "string | null", + "triggerQty": 10, + "freeItemId": "guid | null", + "freeItemName": "string | null", + "freeQty": 2.0, + "bundleComponents": [ + { "itemId": "guid", "sku": "string", "itemName": "string", "qty": 5 } + ], + "regionId": "guid | null", + "regionName": "string | null", + "eligibleSegment": "Gold | null", + "dateRangeFrom": "datetime", + "dateRangeTo": "datetime", + "agentLimitExempt": false, + "baselineExpectedSales": 500000.00, + "isActive": true, + "createdAt": "datetime" +} +``` + +#### Eligible Schemes Response — `GET /api/schemes/eligible?storeId=...&orderId=...` +Array of: +```json +{ + "schemeId": "guid", + "schemeName": "string", + "ruleType": "SlabDiscount", + "eligible": true, + "reason": "string | null", + "discountAmount": 1500.00, + "freeItemId": "guid | null", + "freeQty": 2.0 +} +``` + +#### Scheme Spend — `GET /api/schemes/{id}/spend` +```json +{ + "schemeId": "guid", + "totalDiscountSpend": 15000.00, + "totalFreeGoodsCostSpend": 3000.00, + "totalSpend": 18000.00, + "redemptionCount": 45 +} +``` + +#### Scheme ROI — `GET /api/schemes/{id}/roi` +```json +{ + "schemeId": "guid", + "baselineExpectedSales": 500000.00, + "actualSales": 650000.00, + "incrementalSales": 150000.00, + "totalSpend": 18000.00, + "roi": 7.33 // (incrementalSales - totalSpend) / totalSpend; null if baseline not set +} +``` + +--- + +### 6.17 Suppliers + +**Base path:** `/api/suppliers` +**Required:** JWT token (no specific permission policy) + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/suppliers` | List all suppliers | +| `GET` | `/api/suppliers/{id}` | Get supplier by ID | +| `POST` | `/api/suppliers` | Create supplier | +| `PUT` | `/api/suppliers/{id}` | Update supplier | +| `DELETE` | `/api/suppliers/{id}` | Delete supplier (returns `409` if has active POs) | +| `POST` | `/api/suppliers/{id}/evaluate` | Trigger supplier performance evaluation | + +**Create/Update:** +```json +{ + "name": "string", + "contactInfo": "string | null" +} +``` + +**Response:** +```json +{ + "id": "guid", + "name": "string", + "contactInfo": "string | null", + "performanceScore": 4.5, // decimal | null; computed by evaluate + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.18 Purchase Orders (PO) + +**Base path:** `/api/purchase-orders` +**Required:** JWT token (no specific permission policy) + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/purchase-orders` | List POs (optional filters) | +| `GET` | `/api/purchase-orders/{id}` | Get PO detail | +| `POST` | `/api/purchase-orders` | Create PO | +| `PUT` | `/api/purchase-orders/{id}` | Update PO lines (while Issued) | +| `POST` | `/api/purchase-orders/{id}/close` | Manually close PO | +| `POST` | `/api/purchase-orders/{id}/cancel` | Cancel entire PO | +| `POST` | `/api/purchase-orders/{id}/items/{itemId}/cancel` | Cancel a single PO line | +| `POST` | `/api/purchase-orders/{id}/match-invoice` | Match PO to GRN/Invoice | +| `POST` | `/api/purchase-orders/{id}/resolve-variance` | Resolve invoice variance | + +#### List POs — `GET /api/purchase-orders` +Optional query filters: +| Param | Type | Description | +|---|---|---| +| `supplierId` | `guid` | Filter by supplier | +| `status` | `PurchaseOrderStatus` | Filter by status | + +#### Create PO — `POST /api/purchase-orders` +```json +{ + "supplierId": "guid", + "items": [ + { + "itemId": "guid", + "quantity": 100, + "unitCost": 85.00 + } + ] +} +``` + +#### Update PO — `PUT /api/purchase-orders/{id}` +Only valid while status is `Issued`. +```json +{ + "items": [ + { + "purchaseOrderItemId": "guid | null", // null = new line, guid = update existing + "itemId": "guid", + "quantity": 120, + "unitCost": 83.00 + } + ] +} +``` + +#### Cancel PO — `POST /api/purchase-orders/{id}/cancel` +```json +{ "reason": "string" } +``` + +#### Cancel PO Line — `POST /api/purchase-orders/{id}/items/{itemId}/cancel` +`itemId` here is the `PurchaseOrderItem.Id` (not the item's SKU ID). +```json +{ "reason": "string | null" } +``` + +#### Match Invoice — `POST /api/purchase-orders/{id}/match-invoice` +Query params: `?invoiceId={guid}&grnId={guid}` (both optional) + +**Response:** +```json +{ + "purchaseOrderId": "guid", + "goodsReceiptId": "guid | null", + "invoiceId": "guid | null", + "isMatched": true, + "lines": [ + { + "purchaseOrderItemId": "guid", + "itemId": "guid", + "orderedQuantity": 100, + "acceptedQuantity": 98, + "unitCost": 85.00, + "quantityMatches": false + } + ], + "note": "string" +} +``` + +#### Resolve Variance — `POST /api/purchase-orders/{id}/resolve-variance` +```json +{ + "variance": "string", // description of the variance + "resolution": "string", // resolution action taken + "adjustedUnitCost": 84.00, // optional adjusted cost + "purchaseOrderItemId": "guid" // optional, which line was adjusted +} +``` + +**Response:** +```json +{ + "purchaseOrderId": "guid", + "variance": "string", + "resolution": "string", + "resolvedAt": "datetime" +} +``` + +#### PO Response +```json +{ + "id": "guid", + "supplierId": "guid", + "supplierName": "string", + "createdByUserId": "guid", + "status": "Issued", + "cancelReason": "string | null", + "isActive": true, + "createdAt": "datetime", + "items": [ + { + "id": "guid", + "itemId": "guid", + "sku": "string", + "itemName": "string", + "quantity": 100, + "unitCost": 85.00, + "receivedQuantity": 50, + "cancelledQuantity": 0, + "isCancelled": false + } + ] +} +``` + +--- + +### 6.19 Goods Receipt Notes (GRN) + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/goods-receipts/{id}` | Get GRN by ID | +| `GET` | `/api/purchase-orders/{id}/goods-receipts` | List GRNs for a PO | +| `POST` | `/api/purchase-orders/{id}/goods-receipts` | Create GRN for PO | + +#### Create GRN — `POST /api/purchase-orders/{id}/goods-receipts` +Returns `409 Conflict` if the PO is not in a receivable status. +```json +{ + "warehouseId": "guid", + "items": [ + { + "purchaseOrderItemId": "guid", // PO line being received + "acceptedQuantity": 98, + "rejectedQuantity": 2 + } + ] +} +``` + +#### GRN Response +```json +{ + "id": "guid", + "purchaseOrderId": "guid", + "receivedByUserId": "guid", + "warehouseId": "guid", + "warehouseName": "string", + "receivedAt": "datetime", + "createdAt": "datetime", + "items": [ + { + "id": "guid", + "purchaseOrderItemId": "guid", + "itemId": "guid", + "sku": "string", + "itemName": "string", + "acceptedQuantity": 98, + "rejectedQuantity": 2 + } + ] +} +``` + +--- + +### 6.20 Purchase Returns + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/purchase-returns/{id}` | Get purchase return by ID | +| `GET` | `/api/purchase-orders/{id}/returns` | List returns for a PO | +| `POST` | `/api/purchase-orders/{id}/returns` | Create purchase return for PO | + +#### Create Purchase Return — `POST /api/purchase-orders/{id}/returns` +Returns `409 Conflict` on business rule violations. +```json +{ + "reason": "string", + "items": [ + { + "itemId": "guid", + "quantity": 5 + } + ] +} +``` + +#### Purchase Return Response +```json +{ + "id": "guid", + "purchaseOrderId": "guid", + "reason": "string", + "isActive": true, + "createdAt": "datetime", + "items": [ + { + "id": "guid", + "itemId": "guid", + "sku": "string", + "itemName": "string", + "quantity": 5 + } + ] +} +``` + +--- + +### 6.21 Stock + +**Base path:** `/api/stock` +**Required:** JWT token (no specific permission policy) + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/stock/balances` | Get stock balances (paginated, filterable) | +| `GET` | `/api/stock/balances/{itemId}/total` | Get total stock across all locations for item | +| `GET` | `/api/stock/movements` | Get stock movement history (paginated, filterable) | +| `POST` | `/api/stock/transfer` | Transfer stock between locations | +| `POST` | `/api/stock/damage` | Record damaged stock write-off | +| `POST` | `/api/stock/customer-return` | Record customer return to vehicle | +| `POST` | `/api/stock/adjustment` | Record stock adjustment | + +#### Get Stock Balances — `GET /api/stock/balances` +Query params (all optional): +| Param | Type | Description | +|---|---|---| +| `locationId` | `guid` | Filter by specific location (warehouse or vehicle ID) | +| `locationType` | `LocationType` | Filter by location type (`Warehouse` or `Vehicle`) | +| `itemId` | `guid` | Filter by item | +| `page` | `int` | Default: 1 | +| `pageSize` | `int` | Default: 20 | + +**Response:** Paginated list of `StockBalanceResponse`: +```json +{ + "page": 1, "pageSize": 20, "total": 100, + "items": [ + { + "itemId": "guid", + "itemName": "string", + "locationId": "guid", + "locationType": "Warehouse", + "quantity": 250.0, + "updatedAt": "datetime" + } + ] +} +``` + +#### Get Item Total — `GET /api/stock/balances/{itemId}/total` +```json +{ + "itemId": "guid", + "itemName": "string", + "totalQuantity": 500.0, + "breakdown": [ + { + "itemId": "guid", + "itemName": "string", + "locationId": "guid", + "locationType": "Vehicle", + "quantity": 250.0, + "updatedAt": "datetime" + } + ] +} +``` + +#### Get Stock Movements — `GET /api/stock/movements` +Query params (all optional): +| Param | Type | Description | +|---|---|---| +| `locationId` | `guid` | Filter by location | +| `itemId` | `guid` | Filter by item | +| `type` | `MovementType` | Filter by movement type | +| `from` | `datetime` | Start date filter | +| `to` | `datetime` | End date filter | +| `page` | `int` | Default: 1 | +| `pageSize` | `int` | Default: 20 | + +**Response:** Paginated list of `StockMovementResponse`: +```json +{ + "items": [ + { + "id": "guid", + "itemId": "guid", + "itemName": "string", + "fromLocationId": "guid | null", + "fromLocationType": "Warehouse | null", + "toLocationId": "guid | null", + "toLocationType": "Vehicle | null", + "quantity": 100.0, + "type": "Transfer", + "reasonCode": "string | null", + "referenceId": "guid | null", // e.g. SaleId, RideId, etc. + "createdAt": "datetime", + "createdBy": "guid" + } + ] +} +``` + +#### Transfer Stock — `POST /api/stock/transfer` +```json +{ + "itemId": "guid", + "fromLocationId": "guid", + "fromLocationType": "Warehouse", + "toLocationId": "guid", + "toLocationType": "Vehicle", + "quantity": 50.0, + "referenceId": "guid | null" // e.g. loading sheet ID +} +``` + +#### Record Damage — `POST /api/stock/damage` +```json +{ + "itemId": "guid", + "locationId": "guid", + "locationType": "Warehouse", + "quantity": 3.0, + "reasonCode": "string" // e.g. "WaterDamage", "ExpiredGoods" +} +``` + +#### Record Customer Return — `POST /api/stock/customer-return` +```json +{ + "itemId": "guid", + "vehicleId": "guid", // returns go to vehicle stock + "quantity": 2.0, + "referenceId": "guid | null" +} +``` + +#### Record Adjustment — `POST /api/stock/adjustment` +```json +{ + "itemId": "guid", + "locationId": "guid", + "locationType": "Warehouse", + "quantity": -5.0, // negative = reduce stock, positive = add stock + "reasonCode": "string" +} +``` + +--- + +### 6.22 Driver Profiles + +**Base path:** `/api/driver-profiles` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/driver-profiles` | `driver_profiles.view` | List driver profiles (paginated) | +| `GET` | `/api/driver-profiles/{id}` | `driver_profiles.view` | Get driver profile by ID | +| `POST` | `/api/driver-profiles` | `driver_profiles.create` | Create driver profile | +| `PUT` | `/api/driver-profiles/{id}` | `driver_profiles.update` | Update driver profile | +| `DELETE` | `/api/driver-profiles/{id}` | `driver_profiles.deactivate` | Deactivate driver profile | + +#### List — `GET /api/driver-profiles?page=1&pageSize=20` + +#### Create Driver Profile — `POST /api/driver-profiles` +```json +{ + "fK_UserId": "guid", // user to link this profile to + "licenseNumber": "string", + "licenseExpiry": "datetime", + "medicalClearanceExpiry": "datetime" +} +``` + +#### Update Driver Profile — `PUT /api/driver-profiles/{id}` +```json +{ + "licenseNumber": "string", + "licenseExpiry": "datetime", + "medicalClearanceExpiry": "datetime" +} +``` + +#### Driver Profile Response +```json +{ + "id": "guid", + "userId": "guid", + "userFullName": "string", + "licenseNumber": "string", + "licenseExpiry": "datetime", + "medicalClearanceExpiry": "datetime", + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.23 Driver Compliance Config + +**Base path:** `/api/driver-compliance-config` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/driver-compliance-config` | `compliance.manage` | Get current config | +| `PUT` | `/api/driver-compliance-config` | `compliance.manage` | Create or update config | + +This is a singleton resource — only one config record exists system-wide. + +**GET response** (returns `204 No Content` if not yet configured): +```json +{ + "id": "guid", + "enforceLicenseExpiry": true, + "enforceMedicalClearance": true, + "updatedAt": "datetime", + "updatedBy": "guid" +} +``` + +**PUT request:** +```json +{ + "enforceLicenseExpiry": true, + "enforceMedicalClearance": false +} +``` + +--- + +### 6.24 Rides + +**Base path:** `/api/rides` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/rides` | `rides.view` | List rides (paginated, filterable) | +| `GET` | `/api/rides/{id}` | `rides.view` | Get ride detail | +| `POST` | `/api/rides` | `rides.create` | Create ride | +| `PUT` | `/api/rides/{id}/driver` | `rides.manage` | Assign/change driver | +| `POST` | `/api/rides/{id}/open` | `rides.manage` | Transition to Open status | +| `POST` | `/api/rides/{id}/activate` | `rides.manage` | Activate ride (record meter start) | +| `POST` | `/api/rides/{id}/close` | `rides.manage` | Close ride (record meter end) | +| `POST` | `/api/rides/{id}/cancel-request` | `rides.cancel.request` | Raise a cancel request | +| `GET` | `/api/rides/cancel-requests` | `rides.view` | List all cancel requests | +| `POST` | `/api/rides/cancel-requests/{id}/approve` | `rides.cancel.approve` | Approve cancel request | +| `POST` | `/api/rides/cancel-requests/{id}/reject` | `rides.cancel.approve` | Reject cancel request | +| `GET` | `/api/rides/{id}/cancel-request` | `rides.view` | Get cancel request for a ride | +| `GET` | `/api/rides/breakdown-assist/{rideId}` | `rides.view` | Get breakdown assist data | + +#### Ride Lifecycle + +``` +Draft → Open → Active → Closed + ↓ + CancelRequested → Cancelled +``` + +#### List Rides — `GET /api/rides` +Query params (all optional): +| Param | Type | Description | +|---|---|---| +| `status` | `RideStatus` | Filter by status | +| `type` | `RideType` | Filter by ride type | +| `driverProfileId` | `guid` | Filter by driver | +| `vehicleId` | `guid` | Filter by vehicle | +| `page` | `int` | Default: 1 | +| `pageSize` | `int` | Default: 20 | + +#### Create Ride — `POST /api/rides` +```json +{ + "rideType": "Delivery", + "fK_VehicleId": "guid", + "fK_DriverProfileId": "guid", + "fK_RouteId": "guid", + "fK_SourceWarehouseId": "guid | null" +} +``` + +#### Assign Driver — `PUT /api/rides/{id}/driver` +```json +{ "fK_DriverProfileId": "guid" } +``` + +#### Open Ride — `POST /api/rides/{id}/open` +No body required. + +#### Activate Ride — `POST /api/rides/{id}/activate` +```json +{ "meterstart": 12345 } // odometer reading at start (integer) +``` + +#### Close Ride — `POST /api/rides/{id}/close` +```json +{ "meterend": 12567 } // odometer reading at end (integer) +``` + +#### Raise Cancel Request — `POST /api/rides/{id}/cancel-request` +```json +{ + "typeCode": "VehicleBreakdown", // CancelRequestType enum + "reason": "string" +} +``` + +#### List Cancel Requests — `GET /api/rides/cancel-requests` +Query params: `?status={CancelRequestStatus}&page=1&pageSize=20` + +#### Ride Response +```json +{ + "id": "guid", + "rideType": "Delivery", + "status": "Active", + "vehicleId": "guid", + "vehicleNumberPlate": "ABC-1234", + "driverProfileId": "guid", + "driverFullName": "string", + "routeId": "guid", + "routeName": "string", + "sourceWarehouseId": "guid", + "sourceWarehouseName": "string", + "meterstart": 12345, + "meterend": 0, + "startedAt": "datetime | null", + "activatedAt": "datetime | null", + "closedAt": "datetime | null", + "createdAt": "datetime" +} +``` + +#### Cancel Request Response +```json +{ + "id": "guid", + "rideId": "guid", + "typeCode": "VehicleBreakdown", + "reason": "string", + "requestedBy": "guid", + "requestedAt": "datetime", + "status": "Pending", + "approvedBy": "guid | null", + "approvedAt": "datetime | null" +} +``` + +#### Breakdown Assist Response — `GET /api/rides/breakdown-assist/{rideId}` +Returns active rides on the same route with stock levels, and the open orders for the broken-down ride. +```json +{ + "rideId": "guid", + "activeRides": [ + { + "rideId": "guid", + "vehicleId": "guid", + "vehicleNumberPlate": "string", + "driverProfileId": "guid", + "driverFullName": "string", + "routeId": "guid", + "routeName": "string", + "stockLevels": [ + { "itemId": "guid", "itemName": "string", "locationId": "guid", "locationType": "Vehicle", "quantity": 50.0, "updatedAt": "datetime" } + ] + } + ], + "openOrders": [ + { + "orderId": "guid", + "storeId": "guid", + "storeName": "string", + "orderDate": "datetime", + "totalAmount": 25000.00 + } + ] +} +``` + +--- + +### 6.25 Loading Sheets + +**Base path:** `/api/loading-sheets` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/loading-sheets` | `loading.view` | List loading sheets (paginated, filterable) | +| `GET` | `/api/loading-sheets/{id}` | `loading.view` | Get loading sheet detail | +| `POST` | `/api/loading-sheets` | `loading.dispatch` | Create loading sheet for a ride | +| `PUT` | `/api/loading-sheets/{id}/items` | `loading.dispatch` | Set dispatched items | +| `POST` | `/api/loading-sheets/{id}/confirm-dispatch` | `loading.dispatch` | Confirm dispatch from warehouse | +| `PUT` | `/api/loading-sheets/{id}/receipt-items` | `loading.receive` | Set received item quantities | +| `POST` | `/api/loading-sheets/{id}/confirm-receipt` | `loading.receive` | Driver confirms receipt | +| `POST` | `/api/loading-sheets/{id}/resolve` | `loading.resolve` | Resolve quantity discrepancy | + +#### Loading Sheet Lifecycle + +``` +Pending → DispatchConfirmed → ReceiptConfirmed + ↓ (if quantities differ) + DiscrepancyFlagged → Resolved +``` + +#### List Loading Sheets — `GET /api/loading-sheets` +Query params (all optional): +| Param | Type | Description | +|---|---|---| +| `rideId` | `guid` | Filter by ride | +| `status` | `LoadingSheetStatus` | Filter by status | +| `page` | `int` | Default: 1 | +| `pageSize` | `int` | Default: 20 | + +#### Create Loading Sheet — `POST /api/loading-sheets` +```json +{ "fK_RideId": "guid" } +``` + +#### Set Dispatched Items — `PUT /api/loading-sheets/{id}/items` +Replaces all items on the loading sheet. +```json +{ + "items": [ + { "fK_ItemId": "guid", "quantity": 100.0 } + ] +} +``` + +#### Confirm Dispatch — `POST /api/loading-sheets/{id}/confirm-dispatch` +No body required. Transitions status to `DispatchConfirmed`. + +#### Set Receipt Items — `PUT /api/loading-sheets/{id}/receipt-items` +```json +{ + "items": [ + { "fK_ItemId": "guid", "quantityReceived": 98.0 } + ] +} +``` + +#### Confirm Receipt — `POST /api/loading-sheets/{id}/confirm-receipt` +```json +{ "meterstart": 12345 } // driver's vehicle odometer at receipt +``` + +#### Resolve Discrepancy — `POST /api/loading-sheets/{id}/resolve` +```json +{ + "items": [ + { "fK_ItemId": "guid", "quantityResolved": 98.0 } + ], + "meterstart": 12345 +} +``` + +#### Loading Sheet Response +```json +{ + "id": "guid", + "fK_RideId": "guid", + "fK_SourceWarehouseId": "guid", + "warehouseName": "string", + "status": "Pending", + "dispatchConfirmedAt": "datetime | null", + "dispatchConfirmedBy": "guid | null", + "receiptConfirmedAt": "datetime | null", + "receiptConfirmedBy": "guid | null", + "createdAt": "datetime", + "items": [ + { + "id": "guid", + "fK_ItemId": "guid", + "itemName": "string", + "quantityDispatched": 100.0, + "quantityReceived": 98.0, + "quantityResolved": 98.0 + } + ] +} +``` + +--- + +### 6.26 Delivery Tasks + +**Base path:** `/api` (mixed routes — see below) + +| Method | Path | Description | +|---|---|---| +| `POST` | `/api/rides/{rideId}/tasks/generate` | Auto-generate delivery tasks from ride's orders | +| `GET` | `/api/rides/{rideId}/tasks` | List all tasks for a ride | +| `GET` | `/api/delivery-tasks/{id}` | Get delivery task by ID | +| `PUT` | `/api/delivery-tasks/{id}/quantities` | Set fulfilled quantities per item | +| `POST` | `/api/delivery-tasks/{id}/confirm` | Mark task as delivered | +| `POST` | `/api/delivery-tasks/{id}/fail` | Mark task as failed | + +#### Generate Tasks — `POST /api/rides/{rideId}/tasks/generate` +No body. Generates one `DeliveryTask` per store/order on the ride. +Returns `400 Bad Request` if ride is not in a valid state, `404 Not Found` if ride not found. + +**Response:** Array of `DeliveryTaskResponse`. + +#### Set Quantities — `PUT /api/delivery-tasks/{id}/quantities` +```json +{ + "items": [ + { "itemId": "guid", "quantityFulfilled": 8 } + ] +} +``` + +#### Fail Task — `POST /api/delivery-tasks/{id}/fail` +```json +{ "note": "string" } // reason for failure +``` + +#### Delivery Task Response +```json +{ + "id": "guid", + "fK_RideId": "guid", + "fK_OrderId": "guid", + "storeId": "guid", + "storeName": "string", + "sequence": 1, + "status": "Pending", + "customerConfirmed": false, + "confirmedAt": "datetime | null", + "note": "string | null", + "items": [ + { + "itemId": "guid", + "itemName": "string", + "quantity": 10, + "quantityFulfilled": 8, + "unitPrice": 150.00 + } + ], + "createdAt": "datetime" +} +``` + +--- + +### 6.27 Sales + +**Base path:** `/api/sales` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/sales` | `orders.view` | List all sales | +| `GET` | `/api/sales/{id}` | `orders.view` | Get sale detail | +| `GET` | `/api/sales/store/{storeId}` | `orders.view` | Get sales for a store (scope-enforced) | +| `POST` | `/api/sales` | `orders.create` | Create a new sale | + +> `GET /api/sales/store/{storeId}` returns `403 Forbidden` if current user's scope does not include that store. + +#### Create Sale — `POST /api/sales` +```json +{ + "storeId": "guid", + "saleDate": "datetime", + "totalAmount": 45000.00, + "fulfillImmediately": false, // if true, immediately fulfills the linked order + "rideId": "guid | null", // associate with a ride (for mobile sales) + "items": [ + { + "itemId": "guid", + "quantity": 10, + "unitPrice": null // null = use price list; decimal = override price + } + ] +} +``` + +**Returns `403 Forbidden`** if current user's scope doesn't include the store. + +#### Sale Response +```json +{ + "id": "guid", + "saleDate": "datetime", + "storeId": "guid", + "createdByUserId": "guid", + "rideId": "guid | null", + "totalAmount": 45000.00, + "status": "Pending", + "paymentStatus": "Pending", + "fulfillImmediately": false, + "completedAt": "datetime | null", + "orderId": "guid", // the linked Order record + "orderStatus": "Pending", + "orderFulfilledAt": "datetime | null", + "items": [ + { + "itemId": "guid", + "quantity": 10, + "quantityFulfilled": 0, + "unitPrice": 150.00 + } + ], + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.28 Orders + +**Base path:** `/api/orders` + +| Method | Path | Permission | Description | +|---|---|---|---| +| `GET` | `/api/orders` | `orders.view` | List all orders | +| `GET` | `/api/orders/{id}` | `orders.view` | Get order by ID | +| `GET` | `/api/orders/store/{storeId}` | `orders.view` | Get orders for a store (scope-enforced) | +| `POST` | `/api/orders` | `orders.create` | Create order (linked to a sale) | +| `PUT` | `/api/orders/{id}` | `orders.update` | Update order status | +| `DELETE` | `/api/orders/{id}` | `orders.delete` | Delete order | +| `POST` | `/api/orders/{id}/apply-scheme` | — | Apply a scheme to order | + +> `GET /api/orders/store/{storeId}` and `POST /api/orders` return `403 Forbidden` if scope is not satisfied. + +#### Create Order — `POST /api/orders` +```json +{ "saleId": "guid" } +``` + +#### Update Order — `PUT /api/orders/{id}` +```json +{ "status": "Fulfilled" } // OrderStatus enum +``` + +#### Order Response +```json +{ + "id": "guid", + "saleId": "guid", + "orderDate": "datetime", + "storeId": "guid", + "totalAmount": 45000.00, + "status": "Pending", + "fulfilledAt": "datetime | null", + "items": [ + { + "itemId": "guid", + "quantity": 10, + "quantityFulfilled": 0, + "unitPrice": 150.00 + } + ], + "isActive": true, + "createdAt": "datetime" +} +``` + +--- + +### 6.29 Payments + +**Base path:** `/api/sales/{saleId}/payments` + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/sales/{saleId}/payments` | Get all payments for a sale | +| `POST` | `/api/sales/{saleId}/payments` | Record a payment for a sale | + +> `GET` returns `404 Not Found` if the sale doesn't exist. +> `POST` returns `404 Not Found` if the sale doesn't exist, `400 Bad Request` if payment already complete. + +#### Create Payment — `POST /api/sales/{saleId}/payments` +```json +{ + "amount": 25000.00, + "method": "Cash" // PaymentMethod enum +} +``` + +#### Payment Response +```json +{ + "id": "guid", + "saleId": "guid", + "storeId": "guid", + "amount": 25000.00, + "method": "Cash", + "collectedAt": "datetime", + "collectedBy": "guid", // user who recorded the payment + "createdAt": "datetime" +} +``` + +--- + +### 6.30 Sync (ERP Import) + +**Base path:** `/api/sync` +**Required permission:** `items.create` + +These endpoints are for bulk import from an external ERP system. Each payload is idempotent — existing records (matched by `erpId`) are updated; new records are inserted. + +| Method | Path | Description | +|---|---|---| +| `POST` | `/api/sync/uoms` | Bulk upsert units of measure | +| `POST` | `/api/sync/categories` | Bulk upsert categories + subcategories | +| `POST` | `/api/sync/brands` | Bulk upsert brands | +| `POST` | `/api/sync/items` | Bulk upsert items | + +#### Sync UOMs — `POST /api/sync/uoms` +```json +{ + "items": [ + { "erpId": 101, "name": "Piece" } + ] +} +``` + +#### Sync Categories — `POST /api/sync/categories` +```json +{ + "items": [ + { + "erpId": 10, + "name": "Beverages", + "subCategories": [ + { "erpId": 101, "name": "Soft Drinks", "parentErpId": 10 } + ] + } + ] +} +``` + +#### Sync Brands — `POST /api/sync/brands` +```json +{ + "items": [ + { "erpId": 201, "name": "Coca-Cola" } + ] +} +``` + +#### Sync Items — `POST /api/sync/items` +```json +{ + "items": [ + { + "erpId": 5001, + "sku": "CC-330ML", + "name": "Coca-Cola 330ml", + "description": "string | null", + "categoryErpId": 10, + "subCategoryErpId": 101, + "brandErpId": 201, + "baseUomErpId": 101, + "salePrice": 65.00, + "isActive": true + } + ] +} +``` + +#### Sync Result Response (all sync endpoints) +```json +{ + "inserted": 5, + "updated": 12, + "skipped": 0 +} +``` + +--- + +## 7. Business Flows + +End-to-end sequences showing which API calls to make, in order, for each major workflow. Every step maps directly to an endpoint documented in Section 6. + +--- + +### 7.1 Region → Route → Store Setup + +This is the master data foundation. Everything else (rides, sales, delivery) depends on it existing first. + +``` +1. POST /api/regions → creates a region (geographic area) +2. POST /api/routes → creates a route inside that region + (requires regionId) +3. POST /api/stores → creates a store on that route + (requires routeId) +4. POST /api/stores/{id}/kyc-documents → upload KYC docs for the store +5. PUT /api/stores/{id}/kyc/verify → approve/reject KYC + (kycStatus: Verified unlocks full ordering) +``` + +**Key rules:** +- A store belongs to exactly one route; a route belongs to exactly one region. +- Stores with `KycStatus = Pending` can only place orders up to the configured threshold (default 0 — meaning blocked). Verify KYC before the store can transact. +- Users are scoped to regions/routes/stores via `PUT /api/users/{id}/scope`. Scoped users only see and operate on data within their assigned scope. + +--- + +### 7.2 Sale Flow (Sales Run) + +Used when a sales rep visits a store and records a sale on the spot. The sale and its linked order are created atomically. + +``` +1. POST /api/sales + body: { storeId, saleDate, totalAmount, fulfillImmediately, rideId?, items[] } + → creates Sale + Order in one call + → Sale.status = Pending, Order.status = Pending (if fulfillImmediately: false) + → Sale.status = Completed, Order.status = Fulfilled (if fulfillImmediately: true) + +2. (Optional) GET /api/schemes/eligible?storeId=...&orderId=... + → find promotions the store qualifies for + +3. (Optional) POST /api/orders/{orderId}/apply-scheme + body: { schemeId } + → applies discount or free-goods scheme to the order + +4. POST /api/sales/{saleId}/payments + body: { amount, method: "Cash"|"Credit" } + → records a payment; Sale.paymentStatus updates automatically: + Pending → PartiallyPaid → Paid (based on cumulative total vs totalAmount) + → repeat this step for partial payments +``` + +**Price resolution (automatic on sale creation):** +- If `items[].unitPrice` is `null`, the system looks up the active price list for the store's region and uses that price. +- If `unitPrice` is supplied, it must fall within `[minPrice, price]` from the price list. Prices outside this range cause the sale to be rejected (`null` response → treat as validation error). + +**Stock deduction (when `fulfillImmediately: true` + `rideId` supplied):** +- The system checks that the vehicle has sufficient stock for every item. +- Stock is deducted from the vehicle immediately and a `Sale` movement is recorded. +- If any item lacks stock, the entire sale creation fails. + +**Scope enforcement:** +- The creating user must have scope access to the store, otherwise `403 Forbidden` is returned. + +--- + +### 7.3 Delivery Flow (End-to-End Ride Lifecycle) + +The full sequence for a delivery run — from creating the ride to closing it out. + +#### Step 1 — Set up the ride (Draft) +``` +POST /api/rides +body: { rideType: "Delivery", fK_VehicleId, fK_DriverProfileId, fK_RouteId, fK_SourceWarehouseId? } +→ Ride.status = Draft + +PUT /api/rides/{id}/driver ← optional, change driver while still Draft +body: { fK_DriverProfileId } +``` + +#### Step 2 — Open the ride (Draft → Open) +``` +POST /api/rides/{id}/open +→ Ride.status = Open +``` + +**Blocked if:** +- The vehicle already has another Open or Active ride. +- Driver compliance is enforced and the driver's license or medical clearance has expired. + +#### Step 3 — Create & confirm the loading sheet (Open ride required) +``` +POST /api/loading-sheets +body: { fK_RideId } +→ LoadingSheet.status = Pending + (For Delivery rides: items are auto-populated from pending orders on the route. + For Sale rides: items must be added manually in the next step.) + +PUT /api/loading-sheets/{id}/items ← set/adjust dispatched quantities +body: { items: [{ fK_ItemId, quantity }] } + +POST /api/loading-sheets/{id}/confirm-dispatch +→ LoadingSheet.status = DispatchConfirmed + (Warehouse side confirms the goods left the warehouse) +``` + +#### Step 4 — Driver receives stock & ride goes Active +``` +PUT /api/loading-sheets/{id}/receipt-items ← driver records what they actually received +body: { items: [{ fK_ItemId, quantityReceived }] } + +POST /api/loading-sheets/{id}/confirm-receipt +body: { meterstart: 12345 } +→ if quantities match dispatched: + LoadingSheet.status = ReceiptConfirmed + Stock transfers from Warehouse → Vehicle (Load movement recorded) + Ride.status = Active, Ride.meterstart set + +→ if quantities differ: + LoadingSheet.status = DiscrepancyFlagged + (Ride stays Open; must resolve before activation) + +POST /api/loading-sheets/{id}/resolve ← if DiscrepancyFlagged +body: { items: [{ fK_ItemId, quantityResolved }], meterstart } +→ LoadingSheet.status = Resolved + Stock transfers using resolved quantities + Ride.status = Active +``` + +#### Step 5 — Generate delivery tasks & execute deliveries (Active ride, Delivery type only) +``` +POST /api/rides/{rideId}/tasks/generate +→ creates one DeliveryTask per pending order on the route + (idempotent — skips orders that already have a task for this ride) + +GET /api/rides/{rideId}/tasks ← list tasks to display the delivery sequence + +For each task: + PUT /api/delivery-tasks/{id}/quantities + body: { items: [{ itemId, quantityFulfilled }] } + → set how much was actually delivered per item + + POST /api/delivery-tasks/{id}/confirm + → DeliveryTask.status = Delivered (all qty) or PartiallyDelivered + Order.status = Fulfilled or PartiallyFulfilled + Sale.status = Completed (if fully delivered) + Stock deducted from Vehicle (Sale movement recorded) + + — OR — + + POST /api/delivery-tasks/{id}/fail + body: { note: "reason" } + → DeliveryTask.status = Failed (stock not deducted) +``` + +#### Step 6 — Close the ride (Active → Closed) +``` +POST /api/rides/{id}/close +body: { meterend: 12567 } +→ Ride.status = Closed +``` + +**Blocked if:** The vehicle still has stock with quantity > 0. All stock must be returned (via transfer, damage, or customer-return endpoints) before closing. + +--- + +### 7.4 Ride Cancellation Flow + +Applies when a ride is Active and something goes wrong (breakdown, driver issue, etc.). + +``` +POST /api/rides/{id}/cancel-request +body: { typeCode: "VehicleBreakdown", reason: "string" } +→ Ride.status = CancelRequested + RideCancelRequest.status = Pending + +GET /api/rides/cancel-requests?status=Pending ← supervisor polls for pending requests +GET /api/rides/{id}/cancel-request ← check status for a specific ride + +POST /api/rides/cancel-requests/{requestId}/approve +→ RideCancelRequest.status = Approved + Ride.status = Cancelled + (Blocked if vehicle still has stock) + +— OR — + +POST /api/rides/cancel-requests/{requestId}/reject +→ RideCancelRequest.status = Rejected + Ride.status = Active (restored) +``` + +**Breakdown Assist** — when a ride is cancelled mid-route, use this to find a replacement: +``` +GET /api/rides/breakdown-assist/{cancelledRideId} +→ returns: other Active rides on the same route with their current vehicle stock levels + + open orders from the cancelled ride's route +``` + +--- + +### 7.5 Payment Flow + +Payments are recorded against a Sale after it is created. Multiple partial payments are supported. + +``` +1. POST /api/sales ← sale created; paymentStatus = Pending + +2. POST /api/sales/{saleId}/payments + body: { amount: 25000.00, method: "Cash" } + → payment recorded; sale.paymentStatus updated: + sum(payments) == 0 → Pending + sum(payments) < totalAmount → PartiallyPaid + sum(payments) >= totalAmount → Paid + +3. Repeat step 2 for additional instalments. + +4. GET /api/sales/{saleId}/payments ← list all payments for the sale +``` + +**Rules:** +- Cannot record payment against a `Cancelled` sale — returns `400 Bad Request`. +- No upper limit enforced — overpayment is allowed (handle reconciliation in the UI). +- `collectedBy` is automatically set from the authenticated user's ID. + +--- + +### 7.6 Purchase Order Flow (Procurement) + +``` +1. POST /api/purchase-orders + body: { supplierId, items: [{ itemId, quantity, unitCost }] } + → PO.status = Issued + +2. (Optional) PUT /api/purchase-orders/{id} ← edit lines while Issued only + body: { items: [{ purchaseOrderItemId?, itemId, quantity, unitCost }] } + (purchaseOrderItemId null = add new line, guid = update existing) + +3. POST /api/purchase-orders/{id}/goods-receipts ← goods arrive at warehouse + body: { warehouseId, items: [{ purchaseOrderItemId, acceptedQuantity, rejectedQuantity }] } + → GRN created; stock added to Warehouse (PurchaseReceipt movement) + → PO.status = PartiallyReceived or Received (if all lines fully received) + +4. POST /api/purchase-orders/{id}/match-invoice ← optional 3-way match + query: ?grnId=...&invoiceId=... + → returns line-by-line comparison of ordered vs received quantities + +5. (if discrepancy) POST /api/purchase-orders/{id}/resolve-variance + body: { variance, resolution, adjustedUnitCost?, purchaseOrderItemId? } + +6. POST /api/purchase-orders/{id}/close ← manually close when done + → PO.status = Closed + + — OR — + + POST /api/purchase-orders/{id}/cancel ← cancel entire PO (Issued only) + body: { reason } + + POST /api/purchase-orders/{id}/items/{itemId}/cancel ← cancel one line (Issued only) + body: { reason? } +``` + +**Returns:** `GET /api/purchase-orders/{id}/returns` / `POST /api/purchase-orders/{id}/returns` +Record supplier return: `{ reason, items: [{ itemId, quantity }] }` → stock deducted from Warehouse (PurchaseReturn movement). + +--- + +### 7.7 Loading Sheet Flow (Detail) + +The loading sheet controls the physical movement of stock from warehouse to vehicle. It sits between "ride opened" and "ride active". + +``` +Status machine: +Pending ──► DispatchConfirmed ──► ReceiptConfirmed (clean) + └──► DiscrepancyFlagged ──► Resolved +``` + +| Status | Who acts | What happens to stock | +|---|---|---| +| `Pending` | Warehouse staff | No stock movement yet; edit items freely | +| `DispatchConfirmed` | Warehouse staff | Goods physically leave warehouse; no stock change yet | +| `ReceiptConfirmed` | Driver | Stock transferred Warehouse → Vehicle; ride goes Active | +| `DiscrepancyFlagged` | System | Quantities differ; ride stays Open; waiting for resolution | +| `Resolved` | Supervisor | Resolved quantities transferred; ride goes Active | + +**Important:** confirming receipt (or resolving discrepancy) is what actually activates the ride and loads stock onto the vehicle. The `meterstart` odometer reading is recorded at this point. + +--- + +### 7.8 Stock Flow (How Stock Moves) + +Stock is tracked per item per location (Warehouse or Vehicle). Every write operation creates an immutable `StockMovement` record. + +| Trigger | API call | Movement type | Direction | +|---|---|---|---| +| GRN received | `POST /api/purchase-orders/{id}/goods-receipts` | `PurchaseReceipt` | → Warehouse | +| Loading sheet confirmed | `POST /api/loading-sheets/{id}/confirm-receipt` | `Load` | Warehouse → Vehicle | +| Sale fulfilled (immediate) | `POST /api/sales` (fulfillImmediately: true) | `Sale` | Vehicle → (out) | +| Delivery task confirmed | `POST /api/delivery-tasks/{id}/confirm` | `Sale` | Vehicle → (out) | +| Customer returns goods | `POST /api/stock/customer-return` | `CustomerReturn` | → Vehicle | +| Supplier return | `POST /api/purchase-orders/{id}/returns` | `PurchaseReturn` | Warehouse → (out) | +| Manual transfer | `POST /api/stock/transfer` | `Transfer` | Any → Any | +| Damage write-off | `POST /api/stock/damage` | `Damage` | Location → (out) | +| Manual correction | `POST /api/stock/adjustment` | `Adjustment` | Location ± delta | + +**Closing a ride requires zero vehicle stock.** Any remaining stock must be transferred back to a warehouse, recorded as damage, or returned by the customer before `POST /api/rides/{id}/close` will succeed. + +--- + +### 7.9 Scheme / Promotion Flow + +``` +1. POST /api/schemes ← create a promotion + body: { name, ruleType, ruleConfig?, regionId?, eligibleSegment?, + dateRangeFrom, dateRangeTo, agentLimitExempt, baselineExpectedSales?, + ...optional free-goods fields } + +2. (if BuyXGetY or FreeBundle) + PUT /api/schemes/{id}/free-goods-rule + PUT /api/schemes/{id}/bundle-components/{itemId} + +3. At order time: + GET /api/schemes/eligible?storeId=...&orderId=... + → returns which schemes the store qualifies for, with discount/free-goods amounts + +4. POST /api/orders/{orderId}/apply-scheme + body: { schemeId } + → scheme applied to the order; SchemeRedemption recorded + +5. Analytics: + GET /api/schemes/{id}/spend ← total cost of scheme so far + GET /api/schemes/{id}/roi ← ROI vs baseline expected sales +``` + +**Rule types quick reference:** + +| Type | `ruleConfig` JSON | Trigger | Payout | +|---|---|---|---| +| `FlatPercent` | `{"discountPercent": 10.0}` | Any order | % off total | +| `SlabDiscount` | `{"tiers":[{"minOrderAmount":10000,"discountPercent":5},...]}` | Order value reaches slab | % off total | +| `BuyXGetY` | `null` | Buy N of trigger item | Free qty of free item | +| `FreeBundle` | `null` | Buy all bundle components | Free qty of free item | + +--- + +## Appendix A — Common Business Rules + +1. **Store KYC threshold:** A store with `KycStatus = Pending` has an order threshold of `0` (configurable via `StoreManagement:UnverifiedKycOrderThreshold`). Orders for unverified stores may be blocked or limited. + +2. **Credit enforcement:** On sale creation, the system checks `OutstandingBalance + saleAmount ≤ CreditLimit`. Exceeding this will block the sale. + +3. **Driver compliance:** When `EnforceLicenseExpiry = true`, a ride cannot be opened/activated if the assigned driver's license has expired. Same for `EnforceMedicalClearance`. + +4. **Ride state machine:** Transitions are strictly enforced — calling `open` on a non-Draft ride, `activate` on a non-Open ride, etc. returns `400 Bad Request`. + +5. **Loading sheet discrepancy:** When `ConfirmReceipt` is called and received quantities differ from dispatched quantities, the sheet automatically transitions to `DiscrepancyFlagged` instead of `ReceiptConfirmed`. + +6. **PO line cancellation:** Individual lines can be cancelled only while the PO is `Issued`. The entire PO can only be cancelled while `Issued`. + +7. **Scheme eligibility:** A scheme applies to a store only if the store's segment matches `EligibleSegment` (or the scheme has no segment restriction), the scheme is within its date range, and the region matches (or scheme has no region restriction). + +8. **Agent discount ceiling:** A role's `Level` field controls the maximum discount an agent can apply. Schemes with `AgentLimitExempt = true` stack on top regardless of this ceiling. + +9. **Scope enforcement:** Users with an empty scope list for a scope type (regions/routes/stores) are assumed to have **no restriction** for that type (system-wide access). Only explicitly listed IDs restrict access. + +--- + +## Appendix B — Frontend Implementation Notes + +### Token / Session Strategy +> ⚠️ **YET TO IMPLEMENT** — Auth mechanism not yet decided. Do not implement token storage until Section 1 is finalized. + +### Permission Gating +Once auth is implemented, call `GET /api/roles/{roleId}/permissions` on login to pre-fetch all permissions for the session. Cache them client-side and use them to show/hide UI elements. The role ID will come from whatever credential the Auth Service issues. + +### Enum Serialization +The API serializes enums as **strings** by default in ASP.NET Core `System.Text.Json` — verify with your actual deployment. If numbers are returned, map them using the tables in Section 4. + +### GUID Format +Always send GUIDs as lowercase hyphenated strings: `"3fa85f64-5717-4562-b3fc-2c963f66afa6"`. + +### Date Format +Send all dates as ISO 8601 strings. For date-only fields (license expiry), include a time component: `"2026-12-31T00:00:00Z"`. + +### Pagination +For paginated endpoints, always implement `page` / `pageSize` query params in the UI. Default `pageSize` is `20`. + +### File Upload +KYC document upload uses `multipart/form-data`. Do not set `Content-Type` manually when using browser `FormData` — the browser sets it with the boundary automatically. + +### Error Handling +- `400` errors include `{ "error": "descriptive message" }` — surface this to the user. +- `404` — show "not found" state. +- `409` — show the conflict message from `{ "error": "..." }`. +- `401` — redirect to login. +- `403` — show "access denied" / hide the action. From f1c036d1d88a33f5c6e99839e371feee376b144a Mon Sep 17 00:00:00 2001 From: "Kalana B. Thilakarathna" Date: Tue, 25 Aug 2026 16:20:50 +0530 Subject: [PATCH 2/2] frontend initiatewith shadcn uis --- .../.agents/skills/shadcn/SKILL.md | 277 + .../.agents/skills/shadcn/agents/openai.yml | 5 + .../skills/shadcn/assets/shadcn-small.png | Bin 0 -> 1049 bytes .../.agents/skills/shadcn/assets/shadcn.png | Bin 0 -> 3852 bytes distribution_fe/.agents/skills/shadcn/cli.md | 290 + .../.agents/skills/shadcn/customization.md | 209 + .../.agents/skills/shadcn/evals/evals.json | 77 + distribution_fe/.agents/skills/shadcn/mcp.md | 105 + .../.agents/skills/shadcn/registry.md | 277 + .../skills/shadcn/rules/base-vs-radix.md | 306 + .../.agents/skills/shadcn/rules/chat.md | 224 + .../skills/shadcn/rules/composition.md | 213 + .../.agents/skills/shadcn/rules/forms.md | 192 + .../.agents/skills/shadcn/rules/icons.md | 101 + .../.agents/skills/shadcn/rules/styling.md | 185 + .../.claude/skills/shadcn/SKILL.md | 277 + .../.claude/skills/shadcn/agents/openai.yml | 5 + .../skills/shadcn/assets/shadcn-small.png | Bin 0 -> 1049 bytes .../.claude/skills/shadcn/assets/shadcn.png | Bin 0 -> 3852 bytes distribution_fe/.claude/skills/shadcn/cli.md | 290 + .../.claude/skills/shadcn/customization.md | 209 + .../.claude/skills/shadcn/evals/evals.json | 77 + distribution_fe/.claude/skills/shadcn/mcp.md | 105 + .../.claude/skills/shadcn/registry.md | 277 + .../skills/shadcn/rules/base-vs-radix.md | 306 + .../.claude/skills/shadcn/rules/chat.md | 224 + .../skills/shadcn/rules/composition.md | 213 + .../.claude/skills/shadcn/rules/forms.md | 192 + .../.claude/skills/shadcn/rules/icons.md | 101 + .../.claude/skills/shadcn/rules/styling.md | 185 + .../COMMIT_EDITMSG | 1 + .../.git_backup_distribution_fe/FETCH_HEAD | 0 .../.git_backup_distribution_fe/HEAD | 1 + .../.git_backup_distribution_fe/config | 7 + .../.git_backup_distribution_fe/description | 1 + .../hooks/applypatch-msg.sample | 15 + .../hooks/commit-msg.sample | 24 + .../hooks/fsmonitor-watchman.sample | 174 + .../hooks/post-update.sample | 8 + .../hooks/pre-applypatch.sample | 14 + .../hooks/pre-commit.sample | 49 + .../hooks/pre-merge-commit.sample | 13 + .../hooks/pre-push.sample | 53 + .../hooks/pre-rebase.sample | 169 + .../hooks/pre-receive.sample | 24 + .../hooks/prepare-commit-msg.sample | 42 + .../hooks/push-to-checkout.sample | 78 + .../hooks/sendemail-validate.sample | 77 + .../hooks/update.sample | 128 + .../.git_backup_distribution_fe/index | Bin 0 -> 2109 bytes .../.git_backup_distribution_fe/info/exclude | 6 + .../.git_backup_distribution_fe/logs/HEAD | 1 + .../logs/refs/heads/master | 1 + .../03/b693a7f93d0da640b45d5961a6a30c718b3177 | Bin 0 -> 85 bytes .../05/e726d1b4201bc8c7716d2b058279676582e8c0 | Bin 0 -> 239 bytes .../08/8d90049e99bcf4d4c7d4018d36368f0ceb81a8 | Bin 0 -> 141 bytes .../0d/4ba497cebf1e2e01992220edbb40a88abc5212 | Bin 0 -> 125 bytes .../1e/66186dfe120684092c6e3eef77e24022918ac8 | 2 + .../27/0dd7415ba36c38c4e4ea76c8907dd3d3aa2e44 | Bin 0 -> 438 bytes .../2f/8795a936c6fbca951780dd7da3eec156b43084 | Bin 0 -> 92 bytes .../3d/44f4815118cefa955f645c7cf24796c77c4927 | Bin 0 -> 651 bytes .../45/343f1b48bd33af0bf3478283ff4a49b8894a9f | Bin 0 -> 538 bytes .../46/1b0084104385d8cb1d4d6b5f6114033be973fe | 2 + .../70/ca41f00bdd2dabe4fc05bf7232ff04924c374f | Bin 0 -> 90707 bytes .../71/8d6fea4835ec2d246af9800eddb7ffb276240c | Bin 0 -> 9641 bytes .../73/15a79b7d0a246fa8afcb64e85d6426a1958b0c | 2 + .../7b/b15a3e59978f48d191e43804e150f32d9aec44 | 5 + .../8b/d0e39085d5260e7f8faffcad2fdc45e10aef33 | 3 + .../98/ff035b0228a6045943ce431155f3961fd1fe73 | Bin 0 -> 360 bytes .../a8/a2054a152e7f764ca02887b8ec7c9c91a091b3 | Bin 0 -> 182 bytes .../bc/a0e21ac88217b481c589802feb66db29a1b648 | 3 + .../bd/0c391ddd1088e9067844c48835bf4abcd61783 | 2 + .../c5/9e98a0ba69902ee359a9beca04d7661ae5480f | 1 + .../d5/64d0bc3dd917926892c55e3706cc116d5b165e | Bin 0 -> 53 bytes .../de/dd1d16aabc9d9605aa428ab16b7bc81da17b80 | Bin 0 -> 54 bytes .../e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 | Bin 0 -> 15 bytes .../e8/597e11c99aabf1ed29db3012ff2772f0fa8c27 | Bin 0 -> 1028 bytes .../e8/7e24da14e72c8e4b61379587a2c98a8e146b02 | Bin 0 -> 1034 bytes .../ec/d831f8a0b5d801cfe9cd6cf7601ffb710381cd | Bin 0 -> 154 bytes .../f6/2ca4272ccf54d4064f7780d852400c0ac32df1 | Bin 0 -> 88 bytes .../refs/heads/master | 1 + distribution_fe/.gitignore | 36 + distribution_fe/.prettierignore | 7 + distribution_fe/.prettierrc | 11 + distribution_fe/AGENTS.md | 5 + distribution_fe/README.md | 21 + distribution_fe/app/(auth)/login/page.tsx | 14 + distribution_fe/app/(dashboard)/layout.tsx | 24 + distribution_fe/app/(dashboard)/page.tsx | 8 + distribution_fe/app/favicon.ico | Bin 0 -> 25931 bytes distribution_fe/app/globals.css | 129 + distribution_fe/app/layout.tsx | 35 + distribution_fe/components.json | 25 + distribution_fe/components/.gitkeep | 0 .../components/layout/app-sidebar.tsx | 175 + .../components/layout/bottom-nav.tsx | 52 + .../shared/data-table/data-table.tsx | 686 ++ .../data-table/hooks/use-table-export.ts | 73 + .../data-table/hooks/use-table-filter.ts | 33 + .../shared/data-table/hooks/use-table-sort.ts | 31 + .../components/shared/data-table/index.ts | 17 + .../shared/data-table/table-pagination.tsx | 106 + .../shared/data-table/table-toolbar.tsx | 124 + .../components/shared/data-table/types.ts | 117 + .../shared/data-table/utils/nested-value.ts | 11 + distribution_fe/components/theme-provider.tsx | 71 + distribution_fe/components/ui/badge.tsx | 52 + distribution_fe/components/ui/button.tsx | 56 + distribution_fe/components/ui/checkbox.tsx | 29 + distribution_fe/components/ui/collapsible.tsx | 21 + .../components/ui/dropdown-menu.tsx | 267 + distribution_fe/components/ui/input.tsx | 20 + distribution_fe/components/ui/select.tsx | 200 + distribution_fe/components/ui/separator.tsx | 25 + distribution_fe/components/ui/sheet.tsx | 138 + distribution_fe/components/ui/sidebar.tsx | 727 ++ distribution_fe/components/ui/skeleton.tsx | 13 + distribution_fe/components/ui/table.tsx | 116 + distribution_fe/components/ui/tooltip.tsx | 66 + distribution_fe/config/nav.ts | 154 + distribution_fe/contexts/auth-context.tsx | 107 + distribution_fe/eslint.config.mjs | 18 + distribution_fe/hooks/.gitkeep | 0 distribution_fe/hooks/use-mobile.ts | 19 + distribution_fe/hooks/use-permission.ts | 20 + distribution_fe/lib/.gitkeep | 0 distribution_fe/lib/api.ts | 37 + distribution_fe/lib/utils.ts | 6 + distribution_fe/next.config.ts | 5 + distribution_fe/package-lock.json | 9906 +++++++++++++++++ distribution_fe/package.json | 41 + distribution_fe/postcss.config.mjs | 7 + distribution_fe/public/.gitkeep | 0 distribution_fe/skills-lock.json | 11 + distribution_fe/tsconfig.json | 35 + distribution_fe/types/api.ts | 521 + 136 files changed, 20257 insertions(+) create mode 100644 distribution_fe/.agents/skills/shadcn/SKILL.md create mode 100644 distribution_fe/.agents/skills/shadcn/agents/openai.yml create mode 100644 distribution_fe/.agents/skills/shadcn/assets/shadcn-small.png create mode 100644 distribution_fe/.agents/skills/shadcn/assets/shadcn.png create mode 100644 distribution_fe/.agents/skills/shadcn/cli.md create mode 100644 distribution_fe/.agents/skills/shadcn/customization.md create mode 100644 distribution_fe/.agents/skills/shadcn/evals/evals.json create mode 100644 distribution_fe/.agents/skills/shadcn/mcp.md create mode 100644 distribution_fe/.agents/skills/shadcn/registry.md create mode 100644 distribution_fe/.agents/skills/shadcn/rules/base-vs-radix.md create mode 100644 distribution_fe/.agents/skills/shadcn/rules/chat.md create mode 100644 distribution_fe/.agents/skills/shadcn/rules/composition.md create mode 100644 distribution_fe/.agents/skills/shadcn/rules/forms.md create mode 100644 distribution_fe/.agents/skills/shadcn/rules/icons.md create mode 100644 distribution_fe/.agents/skills/shadcn/rules/styling.md create mode 100644 distribution_fe/.claude/skills/shadcn/SKILL.md create mode 100644 distribution_fe/.claude/skills/shadcn/agents/openai.yml create mode 100644 distribution_fe/.claude/skills/shadcn/assets/shadcn-small.png create mode 100644 distribution_fe/.claude/skills/shadcn/assets/shadcn.png create mode 100644 distribution_fe/.claude/skills/shadcn/cli.md create mode 100644 distribution_fe/.claude/skills/shadcn/customization.md create mode 100644 distribution_fe/.claude/skills/shadcn/evals/evals.json create mode 100644 distribution_fe/.claude/skills/shadcn/mcp.md create mode 100644 distribution_fe/.claude/skills/shadcn/registry.md create mode 100644 distribution_fe/.claude/skills/shadcn/rules/base-vs-radix.md create mode 100644 distribution_fe/.claude/skills/shadcn/rules/chat.md create mode 100644 distribution_fe/.claude/skills/shadcn/rules/composition.md create mode 100644 distribution_fe/.claude/skills/shadcn/rules/forms.md create mode 100644 distribution_fe/.claude/skills/shadcn/rules/icons.md create mode 100644 distribution_fe/.claude/skills/shadcn/rules/styling.md create mode 100644 distribution_fe/.git_backup_distribution_fe/COMMIT_EDITMSG create mode 100644 distribution_fe/.git_backup_distribution_fe/FETCH_HEAD create mode 100644 distribution_fe/.git_backup_distribution_fe/HEAD create mode 100644 distribution_fe/.git_backup_distribution_fe/config create mode 100644 distribution_fe/.git_backup_distribution_fe/description create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/applypatch-msg.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/commit-msg.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/fsmonitor-watchman.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/post-update.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/pre-applypatch.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/pre-commit.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/pre-merge-commit.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/pre-push.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/pre-rebase.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/pre-receive.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/prepare-commit-msg.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/push-to-checkout.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/sendemail-validate.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/hooks/update.sample create mode 100644 distribution_fe/.git_backup_distribution_fe/index create mode 100644 distribution_fe/.git_backup_distribution_fe/info/exclude create mode 100644 distribution_fe/.git_backup_distribution_fe/logs/HEAD create mode 100644 distribution_fe/.git_backup_distribution_fe/logs/refs/heads/master create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/03/b693a7f93d0da640b45d5961a6a30c718b3177 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/05/e726d1b4201bc8c7716d2b058279676582e8c0 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/08/8d90049e99bcf4d4c7d4018d36368f0ceb81a8 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/0d/4ba497cebf1e2e01992220edbb40a88abc5212 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/1e/66186dfe120684092c6e3eef77e24022918ac8 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/27/0dd7415ba36c38c4e4ea76c8907dd3d3aa2e44 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/2f/8795a936c6fbca951780dd7da3eec156b43084 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/3d/44f4815118cefa955f645c7cf24796c77c4927 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/45/343f1b48bd33af0bf3478283ff4a49b8894a9f create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/46/1b0084104385d8cb1d4d6b5f6114033be973fe create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/70/ca41f00bdd2dabe4fc05bf7232ff04924c374f create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/71/8d6fea4835ec2d246af9800eddb7ffb276240c create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/73/15a79b7d0a246fa8afcb64e85d6426a1958b0c create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/7b/b15a3e59978f48d191e43804e150f32d9aec44 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/8b/d0e39085d5260e7f8faffcad2fdc45e10aef33 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/98/ff035b0228a6045943ce431155f3961fd1fe73 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/a8/a2054a152e7f764ca02887b8ec7c9c91a091b3 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/bc/a0e21ac88217b481c589802feb66db29a1b648 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/bd/0c391ddd1088e9067844c48835bf4abcd61783 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/c5/9e98a0ba69902ee359a9beca04d7661ae5480f create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/d5/64d0bc3dd917926892c55e3706cc116d5b165e create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/de/dd1d16aabc9d9605aa428ab16b7bc81da17b80 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/e8/597e11c99aabf1ed29db3012ff2772f0fa8c27 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/e8/7e24da14e72c8e4b61379587a2c98a8e146b02 create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/ec/d831f8a0b5d801cfe9cd6cf7601ffb710381cd create mode 100644 distribution_fe/.git_backup_distribution_fe/objects/f6/2ca4272ccf54d4064f7780d852400c0ac32df1 create mode 100644 distribution_fe/.git_backup_distribution_fe/refs/heads/master create mode 100644 distribution_fe/.gitignore create mode 100644 distribution_fe/.prettierignore create mode 100644 distribution_fe/.prettierrc create mode 100644 distribution_fe/AGENTS.md create mode 100644 distribution_fe/README.md create mode 100644 distribution_fe/app/(auth)/login/page.tsx create mode 100644 distribution_fe/app/(dashboard)/layout.tsx create mode 100644 distribution_fe/app/(dashboard)/page.tsx create mode 100644 distribution_fe/app/favicon.ico create mode 100644 distribution_fe/app/globals.css create mode 100644 distribution_fe/app/layout.tsx create mode 100644 distribution_fe/components.json create mode 100644 distribution_fe/components/.gitkeep create mode 100644 distribution_fe/components/layout/app-sidebar.tsx create mode 100644 distribution_fe/components/layout/bottom-nav.tsx create mode 100644 distribution_fe/components/shared/data-table/data-table.tsx create mode 100644 distribution_fe/components/shared/data-table/hooks/use-table-export.ts create mode 100644 distribution_fe/components/shared/data-table/hooks/use-table-filter.ts create mode 100644 distribution_fe/components/shared/data-table/hooks/use-table-sort.ts create mode 100644 distribution_fe/components/shared/data-table/index.ts create mode 100644 distribution_fe/components/shared/data-table/table-pagination.tsx create mode 100644 distribution_fe/components/shared/data-table/table-toolbar.tsx create mode 100644 distribution_fe/components/shared/data-table/types.ts create mode 100644 distribution_fe/components/shared/data-table/utils/nested-value.ts create mode 100644 distribution_fe/components/theme-provider.tsx create mode 100644 distribution_fe/components/ui/badge.tsx create mode 100644 distribution_fe/components/ui/button.tsx create mode 100644 distribution_fe/components/ui/checkbox.tsx create mode 100644 distribution_fe/components/ui/collapsible.tsx create mode 100644 distribution_fe/components/ui/dropdown-menu.tsx create mode 100644 distribution_fe/components/ui/input.tsx create mode 100644 distribution_fe/components/ui/select.tsx create mode 100644 distribution_fe/components/ui/separator.tsx create mode 100644 distribution_fe/components/ui/sheet.tsx create mode 100644 distribution_fe/components/ui/sidebar.tsx create mode 100644 distribution_fe/components/ui/skeleton.tsx create mode 100644 distribution_fe/components/ui/table.tsx create mode 100644 distribution_fe/components/ui/tooltip.tsx create mode 100644 distribution_fe/config/nav.ts create mode 100644 distribution_fe/contexts/auth-context.tsx create mode 100644 distribution_fe/eslint.config.mjs create mode 100644 distribution_fe/hooks/.gitkeep create mode 100644 distribution_fe/hooks/use-mobile.ts create mode 100644 distribution_fe/hooks/use-permission.ts create mode 100644 distribution_fe/lib/.gitkeep create mode 100644 distribution_fe/lib/api.ts create mode 100644 distribution_fe/lib/utils.ts create mode 100644 distribution_fe/next.config.ts create mode 100644 distribution_fe/package-lock.json create mode 100644 distribution_fe/package.json create mode 100644 distribution_fe/postcss.config.mjs create mode 100644 distribution_fe/public/.gitkeep create mode 100644 distribution_fe/skills-lock.json create mode 100644 distribution_fe/tsconfig.json create mode 100644 distribution_fe/types/api.ts diff --git a/distribution_fe/.agents/skills/shadcn/SKILL.md b/distribution_fe/.agents/skills/shadcn/SKILL.md new file mode 100644 index 0000000..1b4c414 --- /dev/null +++ b/distribution_fe/.agents/skills/shadcn/SKILL.md @@ -0,0 +1,277 @@ +--- +name: shadcn +description: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset". +user-invocable: false +allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) +--- + +# shadcn/ui + +A framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI. + +> **IMPORTANT:** Run all CLI commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest` — based on the project's `packageManager`. Examples below use `npx shadcn@latest` but substitute the correct runner for the project. + +## Current Project Context + +```json +!`npx shadcn@latest info --json` +``` + +The JSON above contains the project config and installed components. Use `npx shadcn@latest docs ` to get documentation and example URLs for any component. + +## Principles + +1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too. +2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table. +3. **Use built-in variants before custom styles.** `variant="outline"`, `size="sm"`, etc. +4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`. + +## Critical Rules + +These rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs. + +### Styling & Tailwind → [styling.md](./rules/styling.md) + +- **`className` for layout, not styling.** Never override component colors or typography. +- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`. +- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`. +- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`. +- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`). +- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries. +- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking. + +### Forms & Inputs → [forms.md](./rules/forms.md) + +- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout. +- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`. +- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.** +- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state. +- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading. +- **Field validation uses `data-invalid` + `aria-invalid`.** `data-invalid` on `Field`, `aria-invalid` on the control. For disabled: `data-disabled` on `Field`, `disabled` on the control. + +### Component Structure → [composition.md](./rules/composition.md) + +- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`. +- **Use `asChild` (radix) or `render` (base) for custom triggers.** Check `base` field from `npx shadcn@latest info`. → [base-vs-radix.md](./rules/base-vs-radix.md) +- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className="sr-only"` if visually hidden. +- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`. +- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`. +- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`. +- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load. + +### Use Components, Not Custom Markup → [composition.md](./rules/composition.md) + +- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`. +- **Callouts use `Alert`.** Don't build custom styled divs. +- **Empty states use `Empty`.** Don't build custom empty state markup. +- **Toast follows the project base.** Use `toast` from the `toast` component for + Base UI projects. Use `toast()` from `sonner` for Radix and React Aria + projects. +- **Use `Separator`** instead of `
` or `
`. +- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs. +- **Use `Badge`** instead of custom styled spans. + +### Icons → [icons.md](./rules/icons.md) + +- **Icons in `Button` use `data-icon`.** `data-icon="inline-start"` or `data-icon="inline-end"` on the icon. +- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`. +- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup. + +### Chat & Messaging → [chat.md](./rules/chat.md) + +- **Chat UI composes the chat primitives.** Conversations use `MessageScroller`, rows use `Message`, surfaces use `Bubble`. Never hand-rolled bubble `div`s or a raw scroll container. +- **`MessageScroller` owns scroll behavior.** Streaming follow, anchoring, and jump-to-latest (`MessageScrollerButton`) are built in. Don't write a `useStickToBottom`/`ResizeObserver` hook. +- **Attachments use `Attachment`; system notes and dividers use `Marker`.** Not `Item` cards or `Separator` + a label. + +### CLI + +- **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode `, `preset url `, or `preset open `. For project-aware preset detection, use `npx shadcn@latest preset resolve`. +- **Apply preset codes directly with the CLI.** Use `npx shadcn@latest apply ` for existing projects, or `npx shadcn@latest init --preset ` when initializing. + +## Key Patterns + +These are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above. + +```tsx +// Form layout: FieldGroup + Field, not div + Label. + + + Email + + + + +// Validation: data-invalid on Field, aria-invalid on the control. + + Email + + Invalid email. + + +// Icons in buttons: data-icon, no sizing classes. + + +// Spacing: gap-*, not space-y-*. +
// correct +
// wrong + +// Equal dimensions: size-*, not w-* h-*. + // correct + // wrong + +// Status colors: Badge variants or semantic tokens, not raw colors. ++20.1% // correct ++20.1% // wrong +``` + +## Component Selection + +| Need | Use | +| -------------------------- | --------------------------------------------------------------------------------------------------- | +| Button/action | `Button` with appropriate variant | +| Form inputs | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` | +| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem` | +| Data display | `Table`, `Card`, `Badge`, `Avatar` | +| Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` | +| Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) | +| Feedback | `toast` (Base UI), `sonner` (Radix/Aria), `Alert`, `Progress`, `Skeleton`, `Spinner` | +| Command palette | `Command` inside `Dialog` | +| Charts | `Chart` (wraps Recharts) | +| Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` | +| Empty states | `Empty` | +| Menus | `DropdownMenu`, `ContextMenu`, `Menubar` | +| Tooltips/info | `Tooltip`, `HoverCard`, `Popover` | +| Chat / conversation UI | `MessageScroller`, `Message`, `Bubble`, `Attachment`, `Marker` | + +## Key Fields + +The injected project context contains these key fields: + +- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode. +- **`isRSC`** → when `true`, components using `useState`, `useEffect`, event handlers, or browser APIs need `"use client"` at the top of the file. Always reference this field when advising on the directive. +- **`tailwindVersion`** → `"v4"` uses `@theme inline` blocks; `"v3"` uses `tailwind.config.js`. +- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one. +- **`style`** → component visual treatment (e.g. `nova`, `vega`). +- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props. +- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`. +- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc. +- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA). +- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`). +- **`preset`** → resolved preset code and values for the current project. Use `npx shadcn@latest preset resolve --json` when you only need preset information. + +See [cli.md — `info` command](./cli.md) for the full field reference. + +## Component Docs, Examples, and Usage + +Run `npx shadcn@latest docs ` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content. + +```bash +npx shadcn@latest docs button dialog select +``` + +**When creating, fixing, debugging, or using a component, always run `npx shadcn@latest docs` and fetch the URLs first.** This ensures you're working with the correct API and usage patterns rather than guessing. + +## Workflow + +1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh. +2. **Check installed components first** — before running `add`, always check the `components` list from project context or list the `resolvedPaths.ui` directory. Don't import components that haven't been added, and don't re-add ones already installed. +3. **Find components** — `npx shadcn@latest search`. +4. **Get docs and examples** — run `npx shadcn@latest docs ` to get URLs, then fetch them. Use `npx shadcn@latest view` to browse registry items you haven't installed. To preview changes to installed components, use `npx shadcn@latest add --diff`. +5. **Install or update** — `npx shadcn@latest add`. When updating existing components, use `--dry-run` and `--diff` to preview changes first (see [Updating Components](#updating-components) below). +6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project. +7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on. +8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, `owner/repo`, etc.), ask which registry to use. Never default to a registry on behalf of the user. +9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**? + - **Inspect current preset**: `npx shadcn@latest preset resolve`. Use `--json` when you need structured values. + - **Inspect incoming preset**: `npx shadcn@latest preset decode `. Use `preset url ` or `preset open ` to share or open the preset builder. + - **Overwrite**: `npx shadcn@latest apply `. Overwrites detected components, fonts, and CSS variables. + - **Partial**: `npx shadcn@latest apply --only theme,font`. Updates only the selected preset parts without reinstalling UI components. Supported values are `theme` and `font`; comma-separated combinations are allowed. `icon` is intentionally not supported, because icon changes may require full component reinstall and transforms. + - **Merge**: `npx shadcn@latest init --preset --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually. + - **Skip**: `npx shadcn@latest init --preset --force --no-reinstall`. Only updates config and CSS, leaves components as-is. + - **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base ` explicitly — preset codes do not encode the base. + +## Updating Components + +When the user asks to update a component from upstream while keeping their local changes, use `--dry-run` and `--diff` to intelligently merge. **NEVER fetch raw files from GitHub manually — always use the CLI.** + +1. Run `npx shadcn@latest add --dry-run` to see all files that would be affected. +2. For each file, run `npx shadcn@latest add --diff ` to see what changed upstream vs local. +3. Decide per file based on the diff: + - No local changes → safe to overwrite. + - Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications. + - User says "just update everything" → use `--overwrite`, but confirm first. +4. **Never use `--overwrite` without the user's explicit approval.** + +## Quick Reference + +```bash +# Create a new project. +npx shadcn@latest init --name my-app --preset base-nova +npx shadcn@latest init --name my-app --preset a2r6bw --template vite + +# Create a monorepo project. +npx shadcn@latest init --name my-app --preset base-nova --monorepo +npx shadcn@latest init --name my-app --preset base-nova --template next --monorepo + +# Initialize existing project. +npx shadcn@latest init --preset base-nova +npx shadcn@latest init --defaults # shortcut: --template=next --preset=nova (base style implied) + +# Apply a preset to an existing project. +npx shadcn@latest apply a2r6bw +npx shadcn@latest apply a2r6bw --only theme +npx shadcn@latest apply a2r6bw --only font +npx shadcn@latest apply a2r6bw --only theme,font + +# Inspect preset codes and project preset state. +npx shadcn@latest preset decode a2r6bw +npx shadcn@latest preset url a2r6bw +npx shadcn@latest preset open a2r6bw +npx shadcn@latest preset resolve +npx shadcn@latest preset resolve --json + +# Add components. +npx shadcn@latest add button card dialog +npx shadcn@latest add @magicui/shimmer-button +npx shadcn@latest add owner/repo/item +npx shadcn@latest add --all + +# Preview changes before adding/updating. +npx shadcn@latest add button --dry-run +npx shadcn@latest add button --diff button.tsx +npx shadcn@latest add @acme/form --view button.tsx +npx shadcn@latest add owner/repo/item --dry-run + +# Search registries. +npx shadcn@latest search @shadcn -q "sidebar" +npx shadcn@latest search @tailark -q "stats" +npx shadcn@latest search owner/repo -q "login" +npx shadcn@latest search # all configured registries +npx shadcn@latest search @shadcn -q "menu" -t ui # filter by item type + +# Get component docs and example URLs. +npx shadcn@latest docs button dialog select + +# View registry item details (for items not yet installed). +npx shadcn@latest view @shadcn/button +npx shadcn@latest view owner/repo/item +``` + +**Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma` +**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo) +**Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com). + +## Detailed References + +- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states +- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading +- [rules/chat.md](./rules/chat.md) — MessageScroller, Message, Bubble, Attachment, Marker; streaming, anchoring, jump-to-latest +- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects +- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index +- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion +- [cli.md](./cli.md) — Commands, flags, presets, templates +- [registry.md](./registry.md) — Authoring source registries, `include`, item definitions, dependencies, GitHub registry rules +- [customization.md](./customization.md) — Theming, CSS variables, extending components diff --git a/distribution_fe/.agents/skills/shadcn/agents/openai.yml b/distribution_fe/.agents/skills/shadcn/agents/openai.yml new file mode 100644 index 0000000..ab636da --- /dev/null +++ b/distribution_fe/.agents/skills/shadcn/agents/openai.yml @@ -0,0 +1,5 @@ +interface: + display_name: "shadcn/ui" + short_description: "Manages shadcn/ui components — adding, searching, fixing, debugging, styling, and composing UI." + icon_small: "./assets/shadcn-small.png" + icon_large: "./assets/shadcn.png" diff --git a/distribution_fe/.agents/skills/shadcn/assets/shadcn-small.png b/distribution_fe/.agents/skills/shadcn/assets/shadcn-small.png new file mode 100644 index 0000000000000000000000000000000000000000..547154b97f2298335159c23eec1dac0d147a1246 GIT binary patch literal 1049 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h z6I`{x0%imor0saV%ts)_S>O>_%)r1c48n{Iv*t(uO^eJ7i71Ki^|4CM&(%vz$xlkv ztH>0+>{umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuG zfu4bq9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)Btk zRH0j3nOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXGvxn!lt}p zsJDO~)CbAv8|oS8!_5Y2wE>A*`4?rT0&NDFZ)a!&R*518wZ}#uWI2*!AU*|)0=;U- zWup%dHajlKxQFb(K%V94;uvBfm>Yb$>yU#$kCbgiC(9RwrlwA(rsW(``(6H=zq@!* zUB_~Trk_lTdxPh$yxDTsVY&L;b+6{tRm^+d+gl^OQcCIBok=%y)Vhx@dR1ceL3r{0 z9=2nMXx8DxEkhL{v?(>~8{qDBkfB!u-iDB`| z-5d%Ae82YI%n8Zg9$mRN?o-|V)V*=5b-R>a>}P2{=pcK^B(&B-W{C#Z!Q%b*ueK>n zK6ymYLRneebCKDSzcqHEHFopo-mkH{zIkt2Z0^086Tko7%N)NxoZYSDM1jSUwPpuS zSeq*wzrC2D(tq4pZ#s9QP@Cd8Q^wW3*2cL_519?*;#4`B;+ei#o;C{Kkamm3Jooa; zE$3K-+c(6ShTjelW_B*W5o zcS6z34IR5C-SIF8m(lui^IOy!-~We?b1?PSGniCYR=ZyR^%<1lJzf1=);T3K0RU@N BSy=!8 literal 0 HcmV?d00001 diff --git a/distribution_fe/.agents/skills/shadcn/assets/shadcn.png b/distribution_fe/.agents/skills/shadcn/assets/shadcn.png new file mode 100644 index 0000000000000000000000000000000000000000..b7b6814acc25073e5f48099b1fd3f70c47bfb1c3 GIT binary patch literal 3852 zcmY*c2{@Ep*nY=Y1|x&SAZEs{m}!)<%wS9yONAB@M)rMZgo-eODf<#aqEe(qiK0X# zjeV<7Sz@e_J-dH=Uw_~KpZ7ZNbD!tj&;6X|T<3bv^oi2VQ{31CN!jQ|snzFQu@Q;0Du?| zfb$Q>hHZD6F}v@?{A7+^3dW)SVQ8`OjvI|a z{6nF7;ZWA*ClCgH0WJs)HH;bth37*c5IOtVpZ7;)e9lMRL5Xd-wU$;iu|t*(dB|ufCq)@;pdCk(RDoU7f8jS zP&Fw%scw{13LdO|DW$q z9v$>f^8d4#e=GgR%Ptkqr-S~xZFoMo{SXfTK;ub7{gc641VugDjPn7=AJBO!*mUKucc{7ey~li!vybaIkF z8OboYVX%y|2%P`k*m&i}s=eW8)0q&YJ<-thLrc)HZ?6uc7Fd!$(lWSvH;?Y^+|N2q z(lz!>Ip|<{_a4CnLN%GIxW|*Sl|;4c3?=Q>z5fWEEtp(&BzeAccRS{zf9!64+SAA{ zhe+CT7U8Ox1UUnilJqweOM{d}_w6-|yz}dA=-R7yHrxs|(MLR_`U=AC%0V#PpIuxs z`__xodV3Bl2ayP^m8isMu4iD<`=4UURtHBPsaLp81r-f3$(4vZbRPd}*>wTo@@?zp zjl1KkifF5p%-V<}MTk3`()wm7qcMd8<14WE>Y_YwjB;{Zt!@vJbH&pu!Ye!p{1kAj zlRg2pkl)WBbyj>3e<$)@mP?WW}W-c;_(UQ(}=MJLED9F^U*P1KWxu z1h@&s1?M0%ax_P5Qb>kkPKEH=OKGZn z&-@5MD^|kk?Hzkfz8h$8#{$$OjAyr!+uGtp>ns1^oS7Nlv6k$^tzTdIb%jUtI8F3t zL;LuqJ7-@4UI|Q)HN=Ap;dAmuZY4c&XphavC{kl z)mMY6L)S-94;V|YT3TA7U^;uvS0W!$W`JIb@kNAm0@IKVWJ@R_jgX6wlUd5V%pHIn;=Z~px z-B_MFLx+ldzTQUp5C zkXaznhH18TytzOgD%$iMnLiAtjvnHLkM;?P*3ZFb#cbY&ZF*|R;4V4$85DZMQ#T@| zxuMMPg?cl|hNJ$2S&%fLErMQSzCnzc_QGh3x@I8V&Omf&=_n+EaC}tft^`)^W5-=1 z0bbJ#wK8+=zNaz-V{U?NtJp&%IDd9-xIut--L}@gU{E>B&ym$vT2@wEZq- zzDqmDf)Cr-aZ-5KIYF!Elcq_X%?qXsg}Yhp=^bebcZ-Ucd$p+US;P}(@VrR4_fy6!qx?X)%SIi+LRsDR` zQ8+xRd{In4WLlVjoy5ElKt%?Ykr)cH*G}0^tE=$(#0W?l)_9MV89|sem>Y}t&?fGt zo3o0sdsG9?JhC1-KM}e*&zeb*H$7JoD&0z#=2sH725JmGV`WoVb86exi5p^Lm{B5BlZRd6(bKH`QmJ*u8#(j6IH1 zFf;Li5iqsV=V^irlzzwVJ$xXwz?I8y`i+t-g0?n77Ie}-fBp1{)&Dm1Yk^Fb=7rI4 zAD)SQRv9=Xr%G{qI!)}>rf;lzG))%89I~zF)R~&_Ur)%0>4+7bxbBknHT`^+jNFe%p=t<=q}3W1Y-u+ zi-cN{V#+HsEEB8UQsUt1uE991MvN%&V?(0Mcb~82H!ori&rLm~Xyf>i#u_t=rzHA3 zu?NRljnv7YU;T&V#(OVE$+=^@kaXvs%C3f78E(k$IyZ`)N$NYtLMfphBkV|oSI}7N zEpFrKnA*jL!k5P(%)+Sl^e>hI@Y}jqmvb~@RD`9$!9Jpu-Wf2Cq?hfV{ghuo&AG>9 zcvC>&o43J*WfyXuRWV)Y0~9;i68pPyv&~-%Ysp}xCY)|oF5?x{w~aS zfx&5VFU)gcnMk*q%=jkB9w_qQ)x=v#1Q^_3`uyEfKiD4y$vaOR4n4IpvQmyNXK%#R zU|XV~4Bq=Kr7Zp3#hdF5_MgA#yL-jJ4E5*j2?mhMq17{mD#6f?!#-b>@HA&~uECub zF2reQA9f>OHLzqeZhi$%7fW!n^Y3_Y{C0vfQs0?a(Pcz7Opuz;(9n>$6)c9L`((v~ z@R1+M2kRN}mDuKCk=od(UwR?y%hUbEv$dC0BD&Ir;03Mf&|A#ZFtn&*mnkH8>2w0+imbwLLU77q!A=W ztt$OR$b#$dkIAwM*ZWG{5*zUPkBN$42Kxe)AYe9DV+?(A^_|IoJLzRGKlbpb%n=PVmB`nxX< z)_}Tpg&IoX;4<0!ksDY9f>*dl`c}zSl!(+ujG#<&yu3AEKR)zVKTaK88*+ma)}3)G zx460(7TNMTz(t&DUv)`7lj(r=`!?>~Q}XW3w-sipV!{Trr8`zsvu9NEK9)zO0>v3D zOs=r|`OyjAS{Ea@UIRrr2Y7A?5+5}R4RpF)ee5^F-dhvP6MKyO`kNB4jGC{-$x}`x zXmpAxVV=WR{>!zDxoq7#DN+_BWG_PRr?$Sa(XT@zUL`HuM{I4bZSuo97aF&7tL@4P zO59)SaLA2ZMU3dO5>=P)GiqcIBUy4A2h_IC-7+IROG<+Qqczgo-JyHw-!h2%h8#$y zr;y4>z2J1R4+`7o>#OWA2iiS1m8)q6A}u3d7VBk **IMPORTANT:** Always run commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest`. Check `packageManager` from project context to choose the right one. Examples below use `npx shadcn@latest` but substitute the correct runner for the project. + +> **IMPORTANT:** Only use the flags documented below. Do not invent or guess flags — if a flag isn't listed here, it doesn't exist. The CLI auto-detects the package manager from the project's lockfile; there is no `--package-manager` flag. + +## Contents + +- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build +- Templates: next, vite, start, react-router, astro +- Presets: named, code, URL formats and fields +- Switching presets + +--- + +## Commands + +### `init` — Initialize or create a project + +```bash +npx shadcn@latest init [components...] [options] +``` + +Initializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step. + +| Flag | Short | Description | Default | +| ----------------------- | ----- | --------------------------------------------------------- | ------- | +| `--template