| .. | ||
| backend | ||
| frontend | ||
| migrations | ||
| templates | ||
| module.json | ||
| README.md | ||
Sag (Case) Module
Overview
The Sag module is the process backbone of BMC Hub. All work flows through cases.
Core Concept
One Entity: Case (Sag)
- Tickets are cases
- Tasks are cases
- Projects are cases
- Differences expressed through:
- Relations (links between cases)
- Tags (workflow state and categorization)
- Attached modules (billing, time tracking, etc.)
Architecture
1. Cases (sag_sager)
- Binary status:
åbenorlukket - No workflow embedded in status
- All workflow managed via tags
2. Tags (sag_tags)
- Represent work to be done
- Have state:
openorclosed - Never deleted - only closed when work completes
- Closing a tag = completion of responsibility
3. Relations (sag_relationer)
- First-class data
- Directional:
kilde_sag_id→målsag_id - Transitive
- UI can derive parent/child/chain views
- No stored parent/child duality
4. Soft Deletes
- All deletes are soft:
deleted_at IS NULL - No hard deletes anywhere
Database Schema
sag_sager (Cases)
idSERIAL PRIMARY KEYtitelVARCHAR(255) NOT NULLbeskrivelseTEXTtemplate_keyVARCHAR(100) - used only at creationstatusVARCHAR(50) CHECK (status IN ('åben', 'lukket'))customer_idINT - links to customers tableansvarlig_bruger_idINTcreated_by_user_idINTdeadlineTIMESTAMPcreated_atTIMESTAMP DEFAULT NOW()updated_atTIMESTAMP DEFAULT NOW()deleted_atTIMESTAMP
sag_tags (Tags)
idSERIAL PRIMARY KEYsag_idINT NOT NULL REFERENCES sag_sager(id)tag_navnVARCHAR(100) NOT NULLstateVARCHAR(20) CHECK (state IN ('open', 'closed'))closed_atTIMESTAMPcreated_atTIMESTAMP DEFAULT NOW()deleted_atTIMESTAMP
sag_relationer (Relations)
idSERIAL PRIMARY KEYkilde_sag_idINT NOT NULL REFERENCES sag_sager(id)målsag_idINT NOT NULL REFERENCES sag_sager(id)relationstypeVARCHAR(50) NOT NULLcreated_atTIMESTAMP DEFAULT NOW()deleted_atTIMESTAMPCHECK (kilde_sag_id != målsag_id)
sag_kontakter (Contact Links)
idSERIAL PRIMARY KEYsag_idINT NOT NULL REFERENCES sag_sager(id)contact_idINT NOT NULL REFERENCES contacts(id)roleVARCHAR(50)created_atTIMESTAMP DEFAULT NOW()deleted_atTIMESTAMP- UNIQUE(sag_id, contact_id)
sag_kunder (Customer Links)
idSERIAL PRIMARY KEYsag_idINT NOT NULL REFERENCES sag_sager(id)customer_idINT NOT NULL REFERENCES customers(id)roleVARCHAR(50)created_atTIMESTAMP DEFAULT NOW()deleted_atTIMESTAMP- UNIQUE(sag_id, customer_id)
API Endpoints
Cases CRUD
GET /api/v1/cases- List all casesPOST /api/v1/cases- Create caseGET /api/v1/cases/{id}- Get casePATCH /api/v1/cases/{id}- Update caseDELETE /api/v1/cases/{id}- Soft-delete case
Tags
GET /api/v1/cases/{id}/tags- List tagsPOST /api/v1/cases/{id}/tags- Add tagPATCH /api/v1/cases/{id}/tags/{tag_id}/state- Toggle tag stateDELETE /api/v1/cases/{id}/tags/{tag_id}- Soft-delete tag
Relations
GET /api/v1/cases/{id}/relations- List relationsPOST /api/v1/cases/{id}/relations- Create relationDELETE /api/v1/cases/{id}/relations/{rel_id}- Soft-delete relation
Contacts & Customers
GET /api/v1/cases/{id}/contacts- List linked contactsPOST /api/v1/cases/{id}/contacts- Link contactDELETE /api/v1/cases/{id}/contacts/{contact_id}- Unlink contactGET /api/v1/cases/{id}/customers- List linked customersPOST /api/v1/cases/{id}/customers- Link customerDELETE /api/v1/cases/{id}/customers/{customer_id}- Unlink customer
Search
GET /api/v1/search/cases?q={query}- Search casesGET /api/v1/search/contacts?q={query}- Search contactsGET /api/v1/search/customers?q={query}- Search customers
Bulk Operations
POST /api/v1/cases/bulk- Bulk actions (close, add tag)
Frontend Routes
/cases- List all cases/cases/new- Create new case/cases/{id}- View case details/cases/{id}/edit- Edit case
Usage Examples
Create a Case
import requests
response = requests.post('http://localhost:8001/api/v1/cases', json={
'titel': 'New Project',
'beskrivelse': 'Project description',
'status': 'åben',
'customer_id': 123
})
Add Tag to Case
response = requests.post('http://localhost:8001/api/v1/cases/1/tags', json={
'tag_navn': 'urgent'
})
Close a Tag (Mark Work Complete)
response = requests.patch('http://localhost:8001/api/v1/cases/1/tags/5/state', json={
'state': 'closed'
})
Link Related Case
response = requests.post('http://localhost:8001/api/v1/cases/1/relations', json={
'målsag_id': 42,
'relationstype': 'blokkerer'
})
Relation Types
- relateret - General relation
- afhænger af - This case depends on target
- blokkerer - This case blocks target
- duplikat - This case duplicates target
Orders Integration
Orders are independent entities but gain meaning through relations to cases.
When creating an Order from a Case:
- Create the Order independently
- Create a relation: Case → Order
- Use relationstype:
ordre_oprettetor similar
Orders do not replace cases - they are transactional satellites.
Design Philosophy
"If you think you need a new table or workflow engine, you're probably wrong. Use relations and tags instead."
The Sag module follows these principles:
- Simplicity - One entity, not many
- Flexibility - Relations express any structure
- Traceability - Soft deletes preserve history
- Clarity - Tags make workflow visible