-
Notifications
You must be signed in to change notification settings - Fork 1
Reports and Leases
Two workflows for operations teams sit outside the day-to-day booking flow:
- Booking reports — a paginated, filterable view of every booking on the system, used for billing back to departments, finding who sat where, or investigating no-shows.
- Building leases — a lightweight record of the commercial lease on each building, with document attachments (the signed PDF, the landlord certificate, floor-plan source files).
Both are SUPER_ADMIN-only today. There is no user-facing report; if you want to expose a slice of the data to floor managers, you'll need to wire it up.
Admin → Reports → Bookings.
Backed by GET /api/v1/bookings/report. Paginated, indexed scan — safe to
run over the whole history. Responds with booking rows plus hydrated user,
asset, floor, building, and primary-zone.
| Query param | Type | Effect |
|---|---|---|
from |
ISO-8601 datetime | Lower bound on startsAt. Inclusive. |
to |
ISO-8601 datetime | Upper bound on startsAt. Inclusive. |
userId |
string | Only this user's bookings. |
assetId |
string | Only bookings against this asset. |
floorId |
string | Only bookings on assets on this floor. |
buildingId |
string | Only bookings in this building. If both floorId and buildingId are set, buildingId wins (later override). |
status |
CONFIRMED | CANCELLED | COMPLETED
|
Single-status filter. Omit for all statuses. |
page |
int ≥ 1 | Page number (default 1). |
limit |
int ≥ 1, ≤ 100 | Page size (default 20, max 100). |
{
"data": [
{
"id": "...",
"startsAt": "2026-04-17T09:00:00.000Z",
"endsAt": "2026-04-17T17:00:00.000Z",
"status": "CONFIRMED",
"notes": "...",
"user": { "id": "...", "displayName": "...", "email": "..." },
"asset": {
"id": "...", "name": "A-01",
"floor": { "id": "...", "name": "L1", "building": { "id": "...", "name": "HQ" } },
"primaryZone":{ "id": "...", "name": "Window Row" }
}
}
],
"meta": { "page": 1, "limit": 20, "total": 1423, "totalPages": 72 }
}-
Monthly utilisation — filter
from/toto the month, group byasset.floor.building.nameclient-side. -
Finding a booker retrospectively — filter by
assetId+from/to. Useful when cleaning up a desk after an incident or tracking lost property. -
No-show audit — filter
status=CANCELLEDwith a tightfrom/to; pair with the booking'supdatedAtto see when the cancel happened.
There is no CSV export endpoint today. The admin UI pulls the JSON and
renders a table; if you need a spreadsheet, paste from the table or hit the
API directly (curl … | jq).
GET /api/v1/bookings/report requires SUPER_ADMIN. Floor managers and
building admins don't see the report menu item and can't hit the endpoint.
Admin → Leases (under the selected building, or globally from the admin
sidebar). Stored in the BuildingLease table.
A lease is a lightweight record of who rents the building from whom, for how long, at what price, plus a bundle of attached documents. It does not drive any automation in Roomer — nothing expires a booking when a lease ends, no alerting on renewal dates. It's a filing cabinet.
| Field | Required? | Notes |
|---|---|---|
buildingId |
✓ | Must reference an existing building. |
name |
✓ | Up to 255 chars. Free-form ("HQ 2024-2027", "Annexe sub-lease"). |
startDate |
✓ | ISO-8601 datetime. |
endDate |
ISO-8601 datetime. Omit for open-ended leases. | |
landlord |
Up to 255 chars. Free-form counterparty name. | |
rentAmount |
Positive number. Interpreted as an amount in currency, no period assumed (store monthly/annual in the notes). |
|
currency |
3-letter ISO code. Defaults to AUD. |
|
notes |
Free-form markdown-ish text. |
| Method & path | Purpose |
|---|---|
GET /api/v1/leases?buildingId= |
List all leases, optionally filtered by building. Ordered newest startDate first. |
POST /api/v1/leases |
Create a lease. |
GET /api/v1/leases/:id |
Detail view, including the list of attached documents. |
PUT /api/v1/leases/:id |
Partial update. Pass only the fields you want changed. |
DELETE /api/v1/leases/:id |
Delete the lease and all attached documents (DB rows + disk files). |
POST /api/v1/leases/:id/documents |
Upload one document (see below). |
GET /api/v1/leases/:id/documents/:docId |
Stream the document back with Content-Disposition: attachment. |
DELETE /api/v1/leases/:id/documents/:docId |
Remove a document (DB row + disk file). |
All lease endpoints require SUPER_ADMIN.
Attachments are stored on disk under Roomer's configured storage directory
(STORAGE_DIR, see Production Deployment) in the layout:
<STORAGE_DIR>/leases/<leaseId>/<epoch-millis>-<sanitised-filename>
-
epoch-millisisDate.now()at upload time — guarantees uniqueness even if the same filename is uploaded twice. -
sanitised-filenamereplaces anything outside[a-zA-Z0-9._-]with_. The original filename is preserved inLeaseDocument.filenamefor display and theContent-Dispositionheader on download.
The upload accepts only the following. Both the MIME type and the extension
are checked, plus a magic-byte sniff via checkFileMagic() which rejects
files whose bytes don't match the declared type (so a .pdf with a PNG
header comes back as 400 INVALID_FILE_TYPE).
| Extension | MIME |
|---|---|
.pdf |
application/pdf |
.doc |
application/msword |
.docx |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
.png |
image/png |
.jpg / .jpeg
|
image/jpeg |
There's no size limit enforced at the route level — if you want one, set it
on the reverse proxy (NGINX client_max_body_size, Traefik body limit
middleware).
-
DELETE /api/v1/leases/:id/documents/:docIdremoves one document's DB row and its file on disk. A missing file is ignored (won't fail the request). -
DELETE /api/v1/leases/:idloops through every attached document, unlinks each file from disk (silently skipping already-missing files), and then deletes the lease row. The Prisma cascade onLeaseDocumentcleans up the DB-side document rows.
There is no soft-delete. A deleted lease is gone; keep the originals in your DMS if you need an audit trail.
Non-bookable assets (isBookable = false) are tracked for inventory purposes
alongside leases in the same admin area. They have:
-
Physical status — one of
AVAILABLE,ASSIGNED,MAINTENANCE,RETIRED,DISABLED. Separate from the booking-eligibilitybookingStatusused by bookable assets. -
Purchase date and warranty expiry —
purchaseDate,warrantyExpirytimestamps on the asset. -
Serial number and asset tag — free-text identifiers.
assetTagis unique system-wide. -
Assignment ledger — the
AssetAssignmenttable records temporary check-outs (assignedAt, optionalreturnedAt,notes,assignedById).
Admin → Assets → pick a non-bookable asset → Assign.
| Endpoint | Purpose |
|---|---|
POST /api/v1/assets/:id/assign |
{ userId, notes? }. Creates an AssetAssignment, flips asset.status to ASSIGNED. Rejects with CONFLICT if already ASSIGNED. |
POST /api/v1/assets/:id/unassign |
Closes the open assignment (stamps returnedAt = now), flips asset.status back to AVAILABLE. |
GET /api/v1/assets/:id/history |
Full assignment history ordered newest first. |
GET /api/v1/assets/my-checkouts |
Current user's active check-outs (assets currently assigned to them with returnedAt = null). |
SUPER_ADMIN only for the assign/unassign/history endpoints. The self-checkout view is available to any authenticated user.
Note: this is the physical assignment ledger (laptop checked out to
Alice until Friday). It's different from AssetUserAssignment — the
permanent "this desk belongs to Bob" model for bookable assets, covered in
Zones and Assets.
Install & Run
- Getting Started
- Development Setup
- Production Deployment
- Kubernetes Deployment
- Backup and Recovery
- TLS Configuration
- Configuration Reference
Using Roomer
- Buildings and Floors
- Zones and Assets
- Users, Groups and Permissions
- Booking and Queue
- Bulk CSV Import
- Enterprise Auth
- Email Notifications
- Webhooks
- Reports and Leases
Developer