Dispenser management is the core of HDB Service. Every physical water dispenser in your fleet is represented as aDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/GianlucaBessone/HDB-Service/llms.txt
Use this file to discover all available pages before exploring further.
Dispenser record that carries its identity, current operational status, ownership plant, deployment location, and a rich audit trail of consumable changes, repairs, and location moves. From the moment a unit is registered until it is retired, every event is captured and queryable.
Dispenser registry
Each dispenser is identified by a user-definedid field — typically the alphanumeric label printed or engraved on the physical unit (e.g., DISP-A058). The id must be globally unique across the entire database, and the API will reject a POST /api/dispensers request with 409 Conflict if a duplicate is detected.
The dispenser
id is the value encoded in its QR code, so it should exactly match the label attached to the hardware.| Field | Type | Description |
|---|---|---|
id | String | Required. Globally unique, user-defined identifier (e.g., DISP-A058). |
marca | String | Required. Brand / manufacturer name. |
modelo | String | Required. Model name or number. |
numeroSerie | String? | Optional manufacturer serial number. |
status | DispenserStatus | Current operational state (see below). Always set to BACKUP on creation regardless of schema default. |
plantId | String? | The home plant that owns this dispenser. Persists even when the unit is deployed elsewhere. |
locationId | String? | The active deployment location. null when the unit is in repair, backup, or out of service. |
clientId | String? | The client who owns the dispenser. |
lifecycleMonths | Int | Expected service life in months. Defaults to 60 (5 years). |
fechaCompra | DateTime? | Purchase date. |
notas | String? | Free-text notes. |
POST /api/dispensers, you may also supply an initialConsumables array (each entry with a materialCode) to record consumables that were pre-installed at the factory or warehouse before the first deployment.
Status lifecycle
Every dispenser has astatus field of enum type DispenserStatus. The status drives which operations are available and how the lifecycle clock behaves.
IN_SERVICE
The dispenser is installed at an active location and fully operational. Lifecycle clock is running. Set automatically when a dispenser is assigned to a location.
BACKUP
The dispenser is in reserve storage. Lifecycle is paused — time spent in
BACKUP does not count toward the end-of-life calculation. lifecyclePausedAt is recorded on transition.UNDER_REPAIR
The unit has been pulled from service for repair. The dispenser is removed from its location automatically when this status is set. Used in conjunction with
DispenserRepairHistory.OUT_OF_SERVICE
Permanently or temporarily decommissioned. The unit is not in any location and not accumulating service time.
BLOCKED
Requires an OC (purchase order) release by a
CLIENT_RESPONSIBLE user or ADMIN before it can return to service. A blockedReason string is mandatory when transitioning to this state.IN_TECHNICAL_SERVICE
Sent to an external technical service center for specialized maintenance or calibration.
BLOCKED_WAITING_OC
A variant of
BLOCKED reserved for units awaiting a purchase-order (OC) approval before returning to service. Treated identically to BLOCKED by the release flow — requires dispensers:release_block permission to transition out.Common state transitions
Registration → BACKUP
All dispensers start in
BACKUP status upon creation. They are not yet deployed to any location.BACKUP → IN_SERVICE
Triggered by a successful assignment to a location (
POST /api/dispensers/[id]/assign). The lifecycle clock resumes and accumulated pause days are recorded.IN_SERVICE → UNDER_REPAIR
Set manually via
PUT /api/dispensers/[id]/status. The dispenser is automatically unlinked from its current location and a DispenserLocationHistory entry is closed with removedAt.IN_SERVICE → BLOCKED
Typically triggered when a client raises a compliance issue. Requires a
reason. Email notifications are sent automatically to CLIENT_RESPONSIBLE and CLIENT_REQUESTER users for the affected plant.BLOCKED → IN_SERVICE
Requires the
dispensers:release_block permission (only ADMIN and CLIENT_RESPONSIBLE hold this permission). The blockedReason and blockedAt fields are cleared.Location assignment
A dispenser is deployed to a physical spot in the building hierarchy (Client → Plant → Sector → Location) by calling POST /api/dispensers/[id]/assign.
- One active dispenser per location — if the target location already has an
IN_SERVICEdispenser, the request is rejected with409 Conflict. - Re-assignment guard — if the dispenser is already assigned to a different location, you must pass
"force": trueto close the previous assignment and open a new one. - Lifecycle start — if the dispenser has never been assigned before (
lifecycleStartDateisnull),lifecycleStartDateis set to the current timestamp on first assignment. - Lifecycle resume — if the dispenser is coming from
BACKUP, any accumulated pause days are added tolifecycleAccumulatedPauseDaysandlifecyclePausedAtis cleared.
DispenserLocationHistory record. When a dispenser leaves a location (via re-assignment, repair, or backup), the open history entry is closed by setting removedAt.
Consumable tracking
Every consumable installed in a dispenser is recorded inDispenserConsumableHistory. This gives you a complete audit trail of which materials were in each unit, when they were replaced, and when they expire.
| Field | Description |
|---|---|
materialCode | Internal code from MaterialCatalog. |
nombre | Material display name at time of installation. |
consumableId | Optional link to a specific serialized Consumable stock unit. |
installedAt | Timestamp of installation (default: now()). |
removedAt | Set when the consumable is replaced. null = currently installed. |
expiresAt | Calculated as installedAt + catalog.expirationMonths. null if the material has no expiration. |
installedById | User who performed the installation. |
removedAt = now()) and writes a new DispenserConsumableHistory entry for each consumable marked as used.
Repair history
When a dispenser requires corrective repair, the event is tracked inDispenserRepairHistory. This model is linked to the dispenser and the technician who performed the work.
| Field | Type | Description |
|---|---|---|
descripcion | String | Text description of the problem. |
diagnostico | String? | Technical diagnosis. |
partesUsadas | Json? | Array of { materialCode, nombre, cantidad } objects listing parts consumed. |
costo | Float? | Total repair cost. |
startDate | DateTime | When repair work began. |
endDate | DateTime? | When repair was completed. null = repair in progress. |
technicianId | String | The technician who performed the repair. |
GET /api/dispensers/[id] returns the last 20 repair history records, ordered by startDate descending.
QR codes
Every dispenser is uniquely identifiable by a QR code that encodes itsid. The QR is generated server-side using the qrcode npm library.
The lib/qr.ts helper extractDispenserId can parse QR payloads in three formats:
| Input format | Example | Result |
|---|---|---|
| Full app URL | https://hdb-service.com/qr/DISP-A058 | DISP-A058 |
| Path segment | /dispensers/DISP-A058 | DISP-A058 |
| Raw ID string | DISP-A058 | DISP-A058 |
DISP- prefix before returning.
Lifecycle tracking
HDB Service tracks the operational age of each dispenser, pausing the clock whenever the unit is not in active service.| Field | Description |
|---|---|
lifecycleMonths | Configured service life (default: 60 months). |
lifecycleStartDate | Set on first location assignment. |
lifecyclePausedAt | Timestamp when the unit entered BACKUP status. null if clock is running. |
lifecycleAccumulatedPauseDays | Sum of all days spent in BACKUP across the unit’s lifetime. |
GET /api/dispensers/[id] response includes two computed fields:
lifecycleExpiration— absolute date when the lifecycle ends, calculated aslifecycleStartDate + (lifecycleMonths × 30 days) + lifecycleAccumulatedPauseDays days. If the unit is currently inBACKUP, the days elapsed sincelifecyclePausedAtare also added so the clock effectively stops until it resumes.lifecycleRemainingDays— days remaining until expiration. If the unit is currently paused, days sincelifecyclePausedAtare excluded from the calculation.
Time spent in
UNDER_REPAIR, OUT_OF_SERVICE, BLOCKED, or IN_TECHNICAL_SERVICE does count toward the lifecycle. Only BACKUP status pauses the clock.Required permissions
| Permission | Roles |
|---|---|
dispensers:read | ADMIN, SUPERVISOR, TECHNICIAN, CLIENT_RESPONSIBLE, CLIENT_REQUESTER |
dispensers:write | ADMIN, SUPERVISOR |
dispensers:assign | ADMIN, SUPERVISOR |
dispensers:status | ADMIN, SUPERVISOR, TECHNICIAN |
dispensers:release_block | ADMIN, CLIENT_RESPONSIBLE |