Skip to main content

Documentation 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 management is the core of HDB Service. Every physical water dispenser in your fleet is represented as a 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-defined id 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.
Key fields registered for every dispenser:
FieldTypeDescription
idStringRequired. Globally unique, user-defined identifier (e.g., DISP-A058).
marcaStringRequired. Brand / manufacturer name.
modeloStringRequired. Model name or number.
numeroSerieString?Optional manufacturer serial number.
statusDispenserStatusCurrent operational state (see below). Always set to BACKUP on creation regardless of schema default.
plantIdString?The home plant that owns this dispenser. Persists even when the unit is deployed elsewhere.
locationIdString?The active deployment location. null when the unit is in repair, backup, or out of service.
clientIdString?The client who owns the dispenser.
lifecycleMonthsIntExpected service life in months. Defaults to 60 (5 years).
fechaCompraDateTime?Purchase date.
notasString?Free-text notes.
When a dispenser is created via 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.
// POST /api/dispensers
{
  "id": "DISP-A058",
  "marca": "Purificador SA",
  "modelo": "X-500 Pro",
  "lifecycleMonths": 60,
  "plantId": "plant_abc123",
  "initialConsumables": [
    { "materialCode": "FILTER-CARB-01" }
  ]
}

Status lifecycle

Every dispenser has a status 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

1

Registration → BACKUP

All dispensers start in BACKUP status upon creation. They are not yet deployed to any location.
2

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

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

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

BLOCKED → IN_SERVICE

Requires the dispensers:release_block permission (only ADMIN and CLIENT_RESPONSIBLE hold this permission). The blockedReason and blockedAt fields are cleared.
6

UNDER_REPAIR / OUT_OF_SERVICE → BACKUP

Returns the unit to reserve storage and resumes lifecycle pause tracking.
Transitioning to BLOCKED without providing a reason will return 400 Bad Request. Always supply a clear reason string describing the blocking cause so that CLIENT_RESPONSIBLE users can act on it.

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.
// POST /api/dispensers/DISP-A058/assign
{
  "locationId": "loc_floor3_cafeteria",
  "force": false
}
The assignment endpoint enforces the following business rules:
  • One active dispenser per location — if the target location already has an IN_SERVICE dispenser, the request is rejected with 409 Conflict.
  • Re-assignment guard — if the dispenser is already assigned to a different location, you must pass "force": true to close the previous assignment and open a new one.
  • Lifecycle start — if the dispenser has never been assigned before (lifecycleStartDate is null), lifecycleStartDate is set to the current timestamp on first assignment.
  • Lifecycle resume — if the dispenser is coming from BACKUP, any accumulated pause days are added to lifecycleAccumulatedPauseDays and lifecyclePausedAt is cleared.
Every assignment writes a DispenserLocationHistory record. When a dispenser leaves a location (via re-assignment, repair, or backup), the open history entry is closed by setting removedAt.
// DispenserLocationHistory fields
{
  dispenserId:  string,   // dispenser ID
  locationId:   string,   // location ID
  assignedAt:   DateTime, // when the assignment was made
  removedAt:    DateTime | null, // null = currently active
  assignedById: string | null    // user who performed the assignment
}

Consumable tracking

Every consumable installed in a dispenser is recorded in DispenserConsumableHistory. This gives you a complete audit trail of which materials were in each unit, when they were replaced, and when they expire.
FieldDescription
materialCodeInternal code from MaterialCatalog.
nombreMaterial display name at time of installation.
consumableIdOptional link to a specific serialized Consumable stock unit.
installedAtTimestamp of installation (default: now()).
removedAtSet when the consumable is replaced. null = currently installed.
expiresAtCalculated as installedAt + catalog.expirationMonths. null if the material has no expiration.
installedByIdUser who performed the installation.
The expiresAt field is indexed so you can efficiently query dispensers with expiring consumables and trigger proactive replacement reminders.
When a checklist is submitted for a maintenance visit (see Maintenance), the system automatically closes the current open consumable record (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 in DispenserRepairHistory. This model is linked to the dispenser and the technician who performed the work.
FieldTypeDescription
descripcionStringText description of the problem.
diagnosticoString?Technical diagnosis.
partesUsadasJson?Array of { materialCode, nombre, cantidad } objects listing parts consumed.
costoFloat?Total repair cost.
startDateDateTimeWhen repair work began.
endDateDateTime?When repair was completed. null = repair in progress.
technicianIdStringThe technician who performed the repair.
// Example partesUsadas value
[
  { "materialCode": "PUMP-001", "nombre": "Bomba de presión", "cantidad": 1 },
  { "materialCode": "SEAL-KIT", "nombre": "Kit de sellos", "cantidad": 2 }
]
The detail endpoint 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 its id. 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 formatExampleResult
Full app URLhttps://hdb-service.com/qr/DISP-A058DISP-A058
Path segment/dispensers/DISP-A058DISP-A058
Raw ID stringDISP-A058DISP-A058
IDs are normalized to uppercase and validated against the DISP- prefix before returning.
Any authenticated user can navigate to /qr/scan to open the in-app QR scanner. Scanning a dispenser QR immediately opens its detail page, from where a ticket can be raised in one tap. This is particularly useful for CLIENT_REQUESTER users reporting issues from the floor.

Lifecycle tracking

HDB Service tracks the operational age of each dispenser, pausing the clock whenever the unit is not in active service.
FieldDescription
lifecycleMonthsConfigured service life (default: 60 months).
lifecycleStartDateSet on first location assignment.
lifecyclePausedAtTimestamp when the unit entered BACKUP status. null if clock is running.
lifecycleAccumulatedPauseDaysSum of all days spent in BACKUP across the unit’s lifetime.
The GET /api/dispensers/[id] response includes two computed fields:
  • lifecycleExpiration — absolute date when the lifecycle ends, calculated as lifecycleStartDate + (lifecycleMonths × 30 days) + lifecycleAccumulatedPauseDays days. If the unit is currently in BACKUP, the days elapsed since lifecyclePausedAt are also added so the clock effectively stops until it resumes.
  • lifecycleRemainingDays — days remaining until expiration. If the unit is currently paused, days since lifecyclePausedAt are 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

PermissionRoles
dispensers:readADMIN, SUPERVISOR, TECHNICIAN, CLIENT_RESPONSIBLE, CLIENT_REQUESTER
dispensers:writeADMIN, SUPERVISOR
dispensers:assignADMIN, SUPERVISOR
dispensers:statusADMIN, SUPERVISOR, TECHNICIAN
dispensers:release_blockADMIN, CLIENT_RESPONSIBLE

Build docs developers (and LLMs) love