HOCO B2B
SRSAPI referenceBack-office endpoints

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.

PermissionUnlocks on this page
dashboard.viewThe 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.administerChanging 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.

MethodPathOperationAccessAppsRelease
GET/admin/dashboard/kpisadmin.dashboard.kpis
KPI cards for a period and filters (permission-aware)
dashboard.viewBOV1
GET/admin/dashboard/charts/{chartKey}admin.dashboard.charts
Series for a dashboard chart (revenue trend, orders by status, top products, conversion funnel…)
dashboard.viewBOV1
GET/admin/dashboard/layoutadmin.dashboard.layout.get
Card layout for my role
dashboard.viewBOV1
PUT/admin/dashboard/layout/{roleKey}admin.dashboard.layout.update
Configure the cards of a role
settings.administerBOV1

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

KeyCard (French UI title)FormulaPeriodExtra permission
revenue.todayCA 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.todayorders.view · reports.view
revenue.monthCA du moisSame sum, month to date. Compared with the previous month over the same number of days.month to dateorders.view · reports.view
revenue.yearCA de l'annéeSame sum, year to date. Compared with the previous year up to the same date.year to dateorders.view · reports.view
revenue.periodCA de la périodeSame sum over from–to. Compared with the previous period of equal length.from–toorders.view · reports.view
orders.toConfirmCommandes à confirmerOrders 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.snapshotorders.view
orders.byStatusCommandes par statutOpen statuses (SUBMITTED, PENDING, VALIDATED, PREPARING, READY, SHIPPED): count now. Terminal statuses (DELIVERED, REFUSED, CANCELLED, RETURNED): count of orders that reached them during the period.mixedorders.view · reports.view
orders.averageBasketPanier moyenrevenue.period ÷ number of orders counted in it (HT).from–toorders.view · reports.view
orders.firstOrdersPremières commandesOrders with isFirstOrder = true submitted in the period and not REFUSED/CANCELLED; assisted = those with channel DELEGATE_ASSISTED.from–toorders.view · reports.view
resellers.activeRevendeurs actifsResellers 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 tocrm.view · reports.view
resellers.newNouveaux revendeursAccounts approved during the period (ValidationStatus → APPROVED); signups = accounts created in the period (any validation status).from–tocrm.view · reports.view
resellers.inactiveRevendeurs inactifsApproved 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 tocrm.view · reports.view
field.delegatesOnTourDélégués en tournéeDelegates whose tour of today is IN_PROGRESS or SUSPENDED; withTourToday = delegates with a tour dated today not CANCELLED; activeDelegates = delegate accounts ACTIVE.snapshotfield.view · reports.view
field.visitsVisites réaliséesdone = visits DONE of tours dated in the period; planned = visits of those tours except CANCELLED; notDone = NOT_DONE; rateBps = done ÷ planned.from–tofield.view · reports.view
field.prospectConversionTaux de conversionCohort 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–tofield.view · crm.view · reports.view
catalog.bestSellersMeilleures ventesTop 5 variants by units on order lines counted in revenue.period, with their revenue HT.from–toorders.view · reports.view
stock.alertsAlertes de stockVariant × warehouse rows of active products with alertLevel OUT (available ≤ 0) and LOW (0 < available ≤ alert threshold).snapshotstock.view
finance.expectedPaymentsPaiements 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.creditUsedCrédit utiliséΣ used of credit accounts with a limit above 0 (formula); limitTotal; utilisationBps; accountsOverLimit.snapshotfinance.view
finance.unpaidImpayésCount and Σ balance of OVERDUE invoices, with aging buckets 1-30, 31-60, 61-90, 90+ days past due.snapshotfinance.view
finance.pendingDeclarationsDéclarations à vérifierCount and Σ amount of payments PENDING_VERIFICATION.snapshotfinance.view
afterSales.openSAV et retours en coursOpen AFTER_SALES tickets (not RESOLVED, REJECTED, CLOSED) and return requests REQUESTED, APPROVED or IN_TRANSIT.snapshotsupport.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.

FilterRevenue, basket, best sellers, first ordersOrders to confirm / by statusResellersFieldStockFinanceAfter-sales
wilayaCode[], communeId[]Order delivery addressOrder delivery addressStore addressVisited reseller's storeignoredInvoice reseller's storeReseller's store
delegateId[]Delegate attributed to the order (sourceDelegateId, else the reseller's assigned delegate at submission)SameAssigned delegateTour delegateignoredReseller's assigned delegateReseller's assigned delegate
brandId[], categoryId[]Only lines of those brands / categories (a category includes its sub-categories)Orders containing at least one such lineignoredignoredVariants of those brands / categoriesignoredClaims 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

QueryTypeValues / defaultNotes
fromstring (date)default: first day of the current monthInclusive, Africa/Algiers
tostring (date)default: todayInclusive; to − from ≤ 366 days (422 REPORT_RANGE_TOO_LARGE)
cardsstring[]card keys; default: the caller's layoutUnknown key → 400 VALIDATION_FAILED; a key the caller may not see is silently dropped
wilayaCodestring[]e.g. 31Repeatable
communeIdUuid[]Repeatable; must belong to the given wilayas when both are sent
delegateIdUuid[]Repeatable
brandIdUuid[]Repeatable
categoryIdUuid[]Repeatable; includes sub-categories
comparebooleantruefalse 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.

  • cards is an object keyed by card key; a card the caller may not see is absent (not null). The Direction role, for example, typically gets no finance.pendingDeclarations.
  • unit is MONEY (centimes), COUNT or BPS. deltaBps = (value − previous) ÷ previous in basis points, null when previous is 0 or unavailable (no data for the comparison period, as for the first year above).
  • comparison is one of SAME_WEEKDAY_PREVIOUS_WEEK, PREVIOUS_MONTH_SAME_DAYS, PREVIOUS_YEAR_TO_DATE, PREVIOUS_PERIOD.
  • catalog.bestSellers returns items instead of value; product names are data and returned as stored.

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown card key, malformed date, from after to, commune outside the given wilayas
403PERMISSION_DENIEDCaller lacks dashboard.view
422REPORT_RANGE_TOO_LARGEPeriod longer than 366 days — details: { maxDays: 366 }

Rules & side effects

  1. 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.
  2. Business days and "today" are computed in Africa/Algiers; a validation at 23:30 UTC on 14 Nov counts on 15 Nov.
  3. 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.
  4. 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.
  5. 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

PathTypeNotes
chartKeystringOne of the keys below; unknown → 404 NOT_FOUND
QueryTypeValues / defaultNotes
from, to, wilayaCode, communeId, delegateId, brandId, categoryIdas for KPIsSame applicability rules as the card the chart belongs to
granularitystringDAY | WEEK | MONTH; default DAY up to 62 days, WEEK up to 183, else MONTHTime-series charts only; weeks start on Sunday (working week Sunday–Thursday)
comparebooleanfalseAdds a previous series over the previous period of equal length
limitnumber10 (max 50)Ranking charts only
Chart keyKindUnitSeriesExtra permission
revenue_trendTIME_SERIESMONEYrevenue (+ previous)orders.view · reports.view
orders_by_statusCATEGORICALCOUNTone point per OrderStatusorders.view · reports.view
orders_by_channelTIME_SERIESCOUNTone series per OrderChannel (digitalisation share, cahier §1.4)orders.view · reports.view
top_productsCATEGORICALMONEYrevenue per variant, with units in metaorders.view · reports.view
revenue_by_brandCATEGORICALMONEYper brandorders.view · reports.view
revenue_by_categoryCATEGORICALMONEYper top-level categoryorders.view · reports.view
revenue_by_wilayaCATEGORICALMONEYper wilaya code (choropleth)orders.view · reports.view
conversion_funnelFUNNELCOUNTprospects → appInstalled → accountsCreated → firstOrders → activefield.view · crm.view · reports.view
visits_trendTIME_SERIESCOUNTplanned, done, notDonefield.view · reports.view
stock_alerts_by_categoryCATEGORICALCOUNTlow, out per categorystock.view
receivables_agingCATEGORICALMONEYnotDue, 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

StatusCodeWhen
400VALIDATION_FAILEDBad filters, granularity on a non-time chart, limit out of range
403PERMISSION_DENIEDCaller lacks dashboard.view or the chart's extra permission
404NOT_FOUNDUnknown chartKey
422REPORT_RANGE_TOO_LARGEPeriod longer than 366 days (DAY granularity: 92 days)

Rules & side effects

  1. Read-only; same definitions as the cards (revenue on validatedAt, HT net of discounts, credit notes subtracted).
  2. Unlike cards, a chart the caller may not see answers 403 — the client only asks for charts of its layout, which is already filtered.
  3. 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

QueryTypeValues / defaultNotes
boardstringGENERAL (default) | SALES | FIELD | STOCK | FINANCEOne 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; size S = 3 columns, M = 4, L = 8, XL = 12; rows are 1 unit high for cards and 3 units for charts.
  • source is ROLE (a configured DashboardLayout) or DEFAULT (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):

RoleCardsCharts
SUPER_ADMINevery cardrevenue_trend, orders_by_status, conversion_funnel, stock_alerts_by_category
COMMERCIAL_ADMINorders to confirm, revenue today/month, average basket, active/new/inactive resellers, delegates on tour, visits, conversion, first orders, best sellersrevenue_trend, conversion_funnel
STOCK_MANAGERstock alerts, orders by status, best sellers, after-salesstock_alerts_by_category, top_products
FINANCE_MANAGERrevenue month/year, expected payments, credit used, unpaid, declarations to verifyreceivables_aging, revenue_trend
DELIVERY_MANAGERorders by status, orders to confirm, after-salesorders_by_status
DIRECTIONrevenue today/month/year, average basket, active/new/inactive resellers, conversion, first orders, unpaidrevenue_trend, revenue_by_wilaya, conversion_funnel

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown board
403PERMISSION_DENIEDCaller lacks dashboard.view (delegates and resellers never have it)

Rules & side effects

  1. Read-only; the layout is cached per role and board and evicted by admin.dashboard.layout.update.
  2. 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

PathTypeNotes
roleKeyRoleKeyA 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

StatusCodeWhen
400VALIDATION_FAILEDUnknown card or chart key, duplicate key, overlapping or out-of-grid positions (col + width > 12), more than 16 cards or 6 charts
403PERMISSION_DENIEDCaller lacks settings.administer
404NOT_FOUNDroleKey is not a back-office role (DELEGATE, RESELLER)
409VERSION_CONFLICTversion is stale — details: { currentVersion }

Rules & side effects

  1. 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 }].
  2. Writes one DashboardLayout row per role and board; AuditLog UPDATE (entity DashboardLayout, label FINANCE_MANAGER/FINANCE) with the before/after lists.
  3. SSE: dashboard.invalidated with layout: true to the connected users of that role, who reload their layout.
  4. No notification.

Used by BO-04 (edit mode) · BO-64

On this page