Dashboards & KPIs
Back-office KPI cards, chart series and role-configurable layouts behind the general and business dashboards, with the exact formula of every indicator.
These four operations feed the general dashboard and the four business dashboards of the back-office (BO-04 to BO-08). They answer cahier §4.1: revenue per day, month and year, orders by status and average basket, active / new / inactive resellers, delegates on tour, visits and conversion, best sellers, stock alerts, expected payments, credit used and unpaid invoices — filtered by date, wilaya, commune, delegate, brand and category, permission-aware and refreshed after business events (ANA-01, ANA-02). Since the 17 Oct 2026 decisions every order waits for a person, so the dashboards also carry the orders to confirm card (ORD-14).
Who uses it. Every back-office role opens a dashboard after sign-in: the Super administrateur and the Administrateur commercial (general and sales dashboards), the Responsable stock (stock dashboard), the Responsable finance (finance dashboard), the Responsable livraison (orders to prepare and ship) and the Direction (read-only KPIs). The cards each user receives are the intersection of their role's layout and their permissions.
| Permission | Unlocks on this page |
|---|---|
dashboard.view | The three read operations. Each card additionally requires the module permission listed in the card catalogue; cards the caller may not see are omitted from the response. |
settings.administer | Changing the layout of a role (admin.dashboard.layout.update). |
Freshness. KPIs are computed from aggregates that the worker updates from OutboxEvents (order, visit, tour, stock, invoice and payment events) — typically within seconds; dataAsOf says how fresh they are. Snapshot cards (orders to confirm, delegates on tour, stock alerts, unpaid) read live tables. After an event that changes cards, the SSE stream sends dashboard.invalidated (at most one per 10 s per user) with the keys of the affected cards; the dashboard re-fetches only those. Nothing here is polled faster than once a minute.
Audit. Reads are not journaled. A layout change writes an AuditLog row UPDATE on DashboardLayout with the before/after card list.
Conventions: amounts are integer centimes of DZD, rates are basis points (1250 = 12,5 %); dates in filters are YYYY-MM-DD in Africa/Algiers; revenue is HT, net of discounts, without delivery fees. Shared schemas (UserSummary, Price, Totals, FileRef…) are on the API index; 401, 429 and 500 apply everywhere and are not repeated.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET | /admin/dashboard/kpis | admin.dashboard.kpisKPI cards for a period and filters (permission-aware) | dashboard.view | BO | V1 |
| GET | /admin/dashboard/charts/{chartKey} | admin.dashboard.chartsSeries for a dashboard chart (revenue trend, orders by status, top products, conversion funnel…) | dashboard.view | BO | V1 |
| GET | /admin/dashboard/layout | admin.dashboard.layout.getCard layout for my role | dashboard.view | BO | V1 |
| PUT | /admin/dashboard/layout/{roleKey} | admin.dashboard.layout.updateConfigure the cards of a role | settings.administer | BO | V1 |
Card catalogue
Every card has a stable key. The table gives its formula (the server computes exactly this; reports use the same definitions), the extra permission (any one of the listed keys, in addition to dashboard.view), and whether the period filter applies or the card is a snapshot of "now".
| Key | Card (French UI title) | Formula | Period | Extra permission |
|---|---|---|---|---|
revenue.today | CA du jour | Σ (subtotalHt − discountTotal) of orders whose validatedAt falls today, minus credit notes issued today; orders now CANCELLED or REFUSED excluded. Compared with the same weekday of the previous week up to the same time. | today | orders.view · reports.view |
revenue.month | CA du mois | Same sum, month to date. Compared with the previous month over the same number of days. | month to date | orders.view · reports.view |
revenue.year | CA de l'année | Same sum, year to date. Compared with the previous year up to the same date. | year to date | orders.view · reports.view |
revenue.period | CA de la période | Same sum over from–to. Compared with the previous period of equal length. | from–to | orders.view · reports.view |
orders.toConfirm | Commandes à confirmer | Orders now SUBMITTED or PENDING, split into new (SUBMITTED), onHold (PENDING except assisted awaiting the reseller) and assisted (PENDING with AWAITING_RESELLER); overdue = waiting longer than orders.confirmationReminderHours (default 4 working hours); oldestSubmittedAt. | snapshot | orders.view |
orders.byStatus | Commandes par statut | Open statuses (SUBMITTED, PENDING, VALIDATED, PREPARING, READY, SHIPPED): count now. Terminal statuses (DELIVERED, REFUSED, CANCELLED, RETURNED): count of orders that reached them during the period. | mixed | orders.view · reports.view |
orders.averageBasket | Panier moyen | revenue.period ÷ number of orders counted in it (HT). | from–to | orders.view · reports.view |
orders.firstOrders | Premières commandes | Orders with isFirstOrder = true submitted in the period and not REFUSED/CANCELLED; assisted = those with channel DELEGATE_ASSISTED. | from–to | orders.view · reports.view |
resellers.active | Revendeurs actifs | Resellers with at least one order validated (and not cancelled) during the crm.inactivityDays days (default 60) ending on to; total = resellers with an approved account. | window ending to | crm.view · reports.view |
resellers.new | Nouveaux revendeurs | Accounts approved during the period (ValidationStatus → APPROVED); signups = accounts created in the period (any validation status). | from–to | crm.view · reports.view |
resellers.inactive | Revendeurs inactifs | Approved resellers with at least one past validated order but none in the crm.inactivityDays window ending on to; neverOrdered = approved resellers without any validated order. | window ending to | crm.view · reports.view |
field.delegatesOnTour | Délégués en tournée | Delegates whose tour of today is IN_PROGRESS or SUSPENDED; withTourToday = delegates with a tour dated today not CANCELLED; activeDelegates = delegate accounts ACTIVE. | snapshot | field.view · reports.view |
field.visits | Visites réalisées | done = visits DONE of tours dated in the period; planned = visits of those tours except CANCELLED; notDone = NOT_DONE; rateBps = done ÷ planned. | from–to | field.view · reports.view |
field.prospectConversion | Taux de conversion | Cohort of prospects created in the period (source DELEGATE_APP, BACK_OFFICE or IMPORT): prospects, accountsCreated (reached ACCOUNT_CREATED by now), firstOrders (reached FIRST_ORDER by now); rateBps = firstOrders ÷ prospects. | cohort from–to | field.view · crm.view · reports.view |
catalog.bestSellers | Meilleures ventes | Top 5 variants by units on order lines counted in revenue.period, with their revenue HT. | from–to | orders.view · reports.view |
stock.alerts | Alertes de stock | Variant × warehouse rows of active products with alertLevel OUT (available ≤ 0) and LOW (0 < available ≤ alert threshold). | snapshot | stock.view |
finance.expectedPayments | Paiements attendus | Σ balance of UNPAID/PARTIALLY_PAID invoices with dueDate between max(from, today) and to, plus codToCollect = Σ totalTtc of SHIPPED orders paid CASH_ON_DELIVERY. | from–to (future part) | finance.view |
finance.creditUsed | Crédit utilisé | Σ used of credit accounts with a limit above 0 (formula); limitTotal; utilisationBps; accountsOverLimit. | snapshot | finance.view |
finance.unpaid | Impayés | Count and Σ balance of OVERDUE invoices, with aging buckets 1-30, 31-60, 61-90, 90+ days past due. | snapshot | finance.view |
finance.pendingDeclarations | Déclarations à vérifier | Count and Σ amount of payments PENDING_VERIFICATION. | snapshot | finance.view |
afterSales.open | SAV et retours en cours | Open AFTER_SALES tickets (not RESOLVED, REJECTED, CLOSED) and return requests REQUESTED, APPROVED or IN_TRANSIT. | snapshot | support.view · orders.view |
Filter applicability. Filters narrow the population each card counts; a filter that has no meaning for a card is ignored for it and listed in that card's ignoredFilters.
| Filter | Revenue, basket, best sellers, first orders | Orders to confirm / by status | Resellers | Field | Stock | Finance | After-sales |
|---|---|---|---|---|---|---|---|
wilayaCode[], communeId[] | Order delivery address | Order delivery address | Store address | Visited reseller's store | ignored | Invoice reseller's store | Reseller's store |
delegateId[] | Delegate attributed to the order (sourceDelegateId, else the reseller's assigned delegate at submission) | Same | Assigned delegate | Tour delegate | ignored | Reseller's assigned delegate | Reseller's assigned delegate |
brandId[], categoryId[] | Only lines of those brands / categories (a category includes its sub-categories) | Orders containing at least one such line | ignored | ignored | Variants of those brands / categories | ignored | Claims or returns with such a line |
GET /admin/dashboard/kpis
admin.dashboard.kpis · Permission dashboard.view (+ per card) · Apps BO · Release V1 · Requirements ANA-01 · ANA-02 · ORD-14
Purpose. Returns the KPI cards of a dashboard for a period and a set of filters, each with its value, comparison and breakdown.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
from | string (date) | default: first day of the current month | Inclusive, Africa/Algiers |
to | string (date) | default: today | Inclusive; to − from ≤ 366 days (422 REPORT_RANGE_TOO_LARGE) |
cards | string[] | card keys; default: the caller's layout | Unknown key → 400 VALIDATION_FAILED; a key the caller may not see is silently dropped |
wilayaCode | string[] | e.g. 31 | Repeatable |
communeId | Uuid[] | Repeatable; must belong to the given wilayas when both are sent | |
delegateId | Uuid[] | Repeatable | |
brandId | Uuid[] | Repeatable | |
categoryId | Uuid[] | Repeatable; includes sub-categories | |
compare | boolean | true | false skips previous and deltaBps (faster) |
Response 200 OK
{
"period": { "from": "2026-11-01", "to": "2026-11-15", "timezone": "Africa/Algiers" },
"filters": { "wilayaCode": ["31"], "communeId": [], "delegateId": [], "brandId": [], "categoryId": [] },
"generatedAt": "2026-11-15T09:30:04Z",
"dataAsOf": "2026-11-15T09:29:58Z",
"cards": {
"revenue.today": {
"unit": "MONEY", "value": 18450000, "previous": 16120000, "deltaBps": 1445,
"comparison": "SAME_WEEKDAY_PREVIOUS_WEEK", "ordersCount": 9, "ignoredFilters": []
},
"revenue.month": {
"unit": "MONEY", "value": 214380000, "previous": 197550000, "deltaBps": 851,
"comparison": "PREVIOUS_MONTH_SAME_DAYS", "ordersCount": 118, "ignoredFilters": []
},
"revenue.year": {
"unit": "MONEY", "value": 3958120000, "previous": null, "deltaBps": null,
"comparison": "PREVIOUS_YEAR_TO_DATE", "ordersCount": 2264, "ignoredFilters": []
},
"orders.toConfirm": {
"unit": "COUNT", "value": 14,
"breakdown": { "new": 9, "onHold": 3, "assisted": 2 },
"overdue": 2, "oldestSubmittedAt": "2026-11-14T07:41:00Z", "ignoredFilters": []
},
"orders.byStatus": {
"unit": "COUNT",
"open": { "SUBMITTED": 9, "PENDING": 5, "VALIDATED": 11, "PREPARING": 6, "READY": 4, "SHIPPED": 17 },
"period": { "DELIVERED": 92, "REFUSED": 3, "CANCELLED": 6, "RETURNED": 1 },
"ignoredFilters": []
},
"orders.averageBasket": { "unit": "MONEY", "value": 1816780, "previous": 1752700, "deltaBps": 366, "ignoredFilters": [] },
"orders.firstOrders": { "unit": "COUNT", "value": 7, "assisted": 4, "previous": 5, "deltaBps": 4000, "ignoredFilters": [] },
"resellers.active": { "unit": "COUNT", "value": 286, "total": 412, "windowDays": 60, "ignoredFilters": [] },
"resellers.new": { "unit": "COUNT", "value": 12, "signups": 15, "previous": 9, "deltaBps": 3333, "ignoredFilters": [] },
"resellers.inactive": { "unit": "COUNT", "value": 94, "neverOrdered": 32, "windowDays": 60, "ignoredFilters": [] },
"field.delegatesOnTour": { "unit": "COUNT", "value": 6, "withTourToday": 8, "activeDelegates": 11, "ignoredFilters": [] },
"field.visits": { "unit": "COUNT", "value": 214, "planned": 251, "notDone": 29, "rateBps": 8526, "ignoredFilters": [] },
"field.prospectConversion": {
"unit": "BPS", "value": 1667, "prospects": 42, "accountsCreated": 19, "firstOrders": 7, "ignoredFilters": []
},
"catalog.bestSellers": {
"unit": "COUNT",
"items": [
{ "variantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "sku": "HC-X14-CC-1M-BK", "productName": "Câble USB-C vers USB-C 60 W 1 m", "brandName": "HOCO", "units": 1840, "revenueHt": 64400000 },
{ "variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "productName": "Chargeur GaN 50 W 2 ports", "brandName": "ACEFAST", "units": 310, "revenueHt": 61380000 }
],
"ignoredFilters": []
},
"stock.alerts": { "unit": "COUNT", "value": 23, "out": 8, "low": 15, "ignoredFilters": ["wilayaCode"] },
"finance.expectedPayments": { "unit": "MONEY", "value": 48620000, "invoicesDue": 31, "codToCollect": 9874600, "ignoredFilters": [] },
"finance.creditUsed": { "unit": "MONEY", "value": 162400000, "limitTotal": 450000000, "utilisationBps": 3609, "accountsOverLimit": 3, "ignoredFilters": [] },
"finance.unpaid": {
"unit": "MONEY", "value": 21730000, "invoices": 17,
"aging": { "1-30": 12480000, "31-60": 6150000, "61-90": 2100000, "90+": 1000000 },
"ignoredFilters": []
},
"finance.pendingDeclarations": { "unit": "MONEY", "value": 5390000, "count": 6, "ignoredFilters": [] },
"afterSales.open": { "unit": "COUNT", "value": 5, "claims": 3, "returns": 2, "ignoredFilters": [] }
}
}Field notes.
cardsis an object keyed by card key; a card the caller may not see is absent (notnull). The Direction role, for example, typically gets nofinance.pendingDeclarations.unitisMONEY(centimes),COUNTorBPS.deltaBps= (value − previous) ÷ previous in basis points,nullwhenpreviousis 0 or unavailable (no data for the comparison period, as for the first year above).comparisonis one ofSAME_WEEKDAY_PREVIOUS_WEEK,PREVIOUS_MONTH_SAME_DAYS,PREVIOUS_YEAR_TO_DATE,PREVIOUS_PERIOD.catalog.bestSellersreturnsitemsinstead ofvalue; product names are data and returned as stored.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown card key, malformed date, from after to, commune outside the given wilayas |
| 403 | PERMISSION_DENIED | Caller lacks dashboard.view |
| 422 | REPORT_RANGE_TOO_LARGE | Period longer than 366 days — details: { maxDays: 366 } |
Rules & side effects
- Read-only. Permission filtering happens before computation: a card the caller may not see is never computed, so its existence costs nothing and leaks nothing.
- Business days and "today" are computed in
Africa/Algiers; a validation at 23:30 UTC on 14 Nov counts on 15 Nov. - Revenue counts an order once, on its
validatedAt(the back-office confirmation), whatever happens later except cancellation or refusal; credit notes (returns, after-sales) are subtracted on their issue date, so returned goods reduce the revenue of the day the credit note is issued. - Orders to confirm include every channel — reseller app, web, assisted and back-office — because no order is confirmed automatically (cahier §4.4, ORD-14). The card links to the queue
admin.orders.toConfirm. - Results for identical parameters are cached 60 s per permission set; an outbox event affecting a card evicts the cache and emits
dashboard.invalidated.
Used by BO-04 · BO-05 · BO-06 · BO-07 · BO-08
GET /admin/dashboard/charts/{chartKey}
admin.dashboard.charts · Permission dashboard.view (+ per chart) · Apps BO · Release V1 · Requirements ANA-01
Purpose. Returns the series of one dashboard chart for the same period and filters as the cards.
Request
| Path | Type | Notes |
|---|---|---|
chartKey | string | One of the keys below; unknown → 404 NOT_FOUND |
| Query | Type | Values / default | Notes |
|---|---|---|---|
from, to, wilayaCode, communeId, delegateId, brandId, categoryId | as for KPIs | Same applicability rules as the card the chart belongs to | |
granularity | string | DAY | WEEK | MONTH; default DAY up to 62 days, WEEK up to 183, else MONTH | Time-series charts only; weeks start on Sunday (working week Sunday–Thursday) |
compare | boolean | false | Adds a previous series over the previous period of equal length |
limit | number | 10 (max 50) | Ranking charts only |
| Chart key | Kind | Unit | Series | Extra permission |
|---|---|---|---|---|
revenue_trend | TIME_SERIES | MONEY | revenue (+ previous) | orders.view · reports.view |
orders_by_status | CATEGORICAL | COUNT | one point per OrderStatus | orders.view · reports.view |
orders_by_channel | TIME_SERIES | COUNT | one series per OrderChannel (digitalisation share, cahier §1.4) | orders.view · reports.view |
top_products | CATEGORICAL | MONEY | revenue per variant, with units in meta | orders.view · reports.view |
revenue_by_brand | CATEGORICAL | MONEY | per brand | orders.view · reports.view |
revenue_by_category | CATEGORICAL | MONEY | per top-level category | orders.view · reports.view |
revenue_by_wilaya | CATEGORICAL | MONEY | per wilaya code (choropleth) | orders.view · reports.view |
conversion_funnel | FUNNEL | COUNT | prospects → appInstalled → accountsCreated → firstOrders → active | field.view · crm.view · reports.view |
visits_trend | TIME_SERIES | COUNT | planned, done, notDone | field.view · reports.view |
stock_alerts_by_category | CATEGORICAL | COUNT | low, out per category | stock.view |
receivables_aging | CATEGORICAL | MONEY | notDue, 1-30, 31-60, 61-90, 90+ | finance.view |
interface ChartResponse {
chartKey: string;
kind: "TIME_SERIES" | "CATEGORICAL" | "FUNNEL";
unit: "MONEY" | "COUNT" | "BPS";
granularity?: "DAY" | "WEEK" | "MONTH"; // TIME_SERIES only
series: {
key: string; // "revenue", "previous", an enum value, a wilaya code…
label?: string; // only for data labels (product, brand, category names) — never for enum values
points: { x: string; y: number; meta?: Record<string, number | string> }[];
}[];
totals?: Record<string, number>;
period: { from: string; to: string };
generatedAt: Iso;
}Response 200 OK — revenue_trend, weekly, with comparison
{
"chartKey": "revenue_trend",
"kind": "TIME_SERIES",
"unit": "MONEY",
"granularity": "WEEK",
"series": [
{ "key": "revenue", "points": [
{ "x": "2026-10-25", "y": 58210000 },
{ "x": "2026-11-01", "y": 71460000 },
{ "x": "2026-11-08", "y": 84730000 }
] },
{ "key": "previous", "points": [
{ "x": "2026-10-25", "y": 52900000 },
{ "x": "2026-11-01", "y": 66150000 },
{ "x": "2026-11-08", "y": 70020000 }
] }
],
"totals": { "revenue": 214400000, "previous": 189070000 },
"period": { "from": "2026-10-25", "to": "2026-11-14" },
"generatedAt": "2026-11-15T09:30:05Z"
}conversion_funnel returns a single series whose points are the steps in order ({ "x": "prospects", "y": 42 }, { "x": "appInstalled", "y": 23 }…). Time points are the first day of each bucket; empty buckets are returned with y: 0 so the client never interpolates.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Bad filters, granularity on a non-time chart, limit out of range |
| 403 | PERMISSION_DENIED | Caller lacks dashboard.view or the chart's extra permission |
| 404 | NOT_FOUND | Unknown chartKey |
| 422 | REPORT_RANGE_TOO_LARGE | Period longer than 366 days (DAY granularity: 92 days) |
Rules & side effects
- Read-only; same definitions as the cards (revenue on
validatedAt, HT net of discounts, credit notes subtracted). - Unlike cards, a chart the caller may not see answers
403— the client only asks for charts of its layout, which is already filtered. - Cached 60 s per parameters and permission set; evicted with the cards.
Used by BO-04 · BO-05 · BO-06 · BO-07 · BO-08
GET /admin/dashboard/layout
admin.dashboard.layout.get · Permission dashboard.view · Apps BO · Release V1 · Requirements ANA-02
Purpose. Returns the cards and charts of the caller's dashboards, as configured for their role and filtered by their permissions.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
board | string | GENERAL (default) | SALES | FIELD | STOCK | FINANCE | One layout per role and board (BO-04 to BO-08) |
Response 200 OK
{
"board": "GENERAL",
"roleKey": "COMMERCIAL_ADMIN",
"source": "ROLE",
"updatedAt": "2026-11-02T10:14:00Z",
"cards": [
{ "key": "orders.toConfirm", "size": "M", "row": 0, "col": 0 },
{ "key": "revenue.today", "size": "S", "row": 0, "col": 4 },
{ "key": "revenue.month", "size": "S", "row": 0, "col": 7 },
{ "key": "orders.averageBasket", "size": "S", "row": 0, "col": 10 },
{ "key": "resellers.active", "size": "S", "row": 1, "col": 0 },
{ "key": "field.delegatesOnTour", "size": "S", "row": 1, "col": 3 },
{ "key": "field.prospectConversion", "size": "S", "row": 1, "col": 6 },
{ "key": "orders.firstOrders", "size": "S", "row": 1, "col": 9 }
],
"charts": [
{ "key": "revenue_trend", "size": "L", "row": 2, "col": 0 },
{ "key": "conversion_funnel", "size": "M", "row": 2, "col": 8 }
],
"defaultFilters": { "period": "MONTH_TO_DATE" }
}Field notes.
- Grid: 12 columns;
sizeS= 3 columns,M= 4,L= 8,XL= 12; rows are 1 unit high for cards and 3 units for charts. sourceisROLE(a configuredDashboardLayout) orDEFAULT(the built-in default below).- A user holding several roles gets the layout of their highest role in this order:
SUPER_ADMIN,COMMERCIAL_ADMIN,FINANCE_MANAGER,STOCK_MANAGER,DELIVERY_MANAGER,DIRECTION; cards and charts they may not see are removed (the remaining ones keep their positions). defaultFilters.period:TODAY,WEEK_TO_DATE,MONTH_TO_DATE,YEAR_TO_DATE.
Built-in defaults of the GENERAL board (used until a role is configured):
| Role | Cards | Charts |
|---|---|---|
SUPER_ADMIN | every card | revenue_trend, orders_by_status, conversion_funnel, stock_alerts_by_category |
COMMERCIAL_ADMIN | orders to confirm, revenue today/month, average basket, active/new/inactive resellers, delegates on tour, visits, conversion, first orders, best sellers | revenue_trend, conversion_funnel |
STOCK_MANAGER | stock alerts, orders by status, best sellers, after-sales | stock_alerts_by_category, top_products |
FINANCE_MANAGER | revenue month/year, expected payments, credit used, unpaid, declarations to verify | receivables_aging, revenue_trend |
DELIVERY_MANAGER | orders by status, orders to confirm, after-sales | orders_by_status |
DIRECTION | revenue today/month/year, average basket, active/new/inactive resellers, conversion, first orders, unpaid | revenue_trend, revenue_by_wilaya, conversion_funnel |
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown board |
| 403 | PERMISSION_DENIED | Caller lacks dashboard.view (delegates and resellers never have it) |
Rules & side effects
- Read-only; the layout is cached per role and board and evicted by
admin.dashboard.layout.update. - The response never contains cards for permissions the caller lacks, so the client renders it as is.
Used by BO-03 · BO-04 · BO-05 · BO-06 · BO-07 · BO-08
PUT /admin/dashboard/layout/{roleKey}
admin.dashboard.layout.update · Permission settings.administer · Apps BO · Release V1 · Requirements ANA-02
Purpose. Replaces the cards and charts of one board for one role (cahier §4.1 « Les cartes du tableau de bord doivent être configurables par rôle »).
Request
| Path | Type | Notes |
|---|---|---|
roleKey | RoleKey | A back-office role: SUPER_ADMIN, COMMERCIAL_ADMIN, STOCK_MANAGER, FINANCE_MANAGER, DELIVERY_MANAGER, DIRECTION |
interface UpdateDashboardLayoutBody {
board: "GENERAL" | "SALES" | "FIELD" | "STOCK" | "FINANCE";
cards: { key: string; size: "S" | "M" | "L" | "XL"; row: number; col: number }[]; // 1–16 cards
charts: { key: string; size: "M" | "L" | "XL"; row: number; col: number }[]; // 0–6 charts
defaultFilters?: { period: "TODAY" | "WEEK_TO_DATE" | "MONTH_TO_DATE" | "YEAR_TO_DATE" };
resetToDefault?: boolean; // true: delete the role's layout and fall back to the built-in default (cards/charts ignored)
version?: number;
}{
"board": "FINANCE",
"cards": [
{ "key": "finance.unpaid", "size": "M", "row": 0, "col": 0 },
{ "key": "finance.expectedPayments", "size": "M", "row": 0, "col": 4 },
{ "key": "finance.pendingDeclarations", "size": "M", "row": 0, "col": 8 },
{ "key": "finance.creditUsed", "size": "M", "row": 1, "col": 0 }
],
"charts": [{ "key": "receivables_aging", "size": "L", "row": 2, "col": 0 }],
"defaultFilters": { "period": "MONTH_TO_DATE" },
"version": 3
}Response 200 OK — the stored layout, same shape as admin.dashboard.layout.get with "source": "ROLE", "roleKey": "FINANCE_MANAGER" and "version": 4.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown card or chart key, duplicate key, overlapping or out-of-grid positions (col + width > 12), more than 16 cards or 6 charts |
| 403 | PERMISSION_DENIED | Caller lacks settings.administer |
| 404 | NOT_FOUND | roleKey is not a back-office role (DELEGATE, RESELLER) |
| 409 | VERSION_CONFLICT | version is stale — details: { currentVersion } |
Rules & side effects
- A layout may contain cards whose permission the role does not hold today; they are kept (the role may get the permission later) and filtered at read time. The response lists them in
warnings: [{ key, missingPermission }]. - Writes one
DashboardLayoutrow per role and board;AuditLogUPDATE(entityDashboardLayout, labelFINANCE_MANAGER/FINANCE) with the before/after lists. - SSE:
dashboard.invalidatedwithlayout: trueto the connected users of that role, who reload their layout. - No notification.
Offline sync & location
How the offline-first delegate app talks to the server: ordered replay of queued mutations with per-item results, the delta feed of its offline scopes with tombstones, and batched location pings during a tour.
CRM
Back-office operations on resellers and prospects — list and map, 360° record, creation with duplicate checks, customer type, lifecycle, assignment, validation queue, timeline, documents, tasks, segments and export.