2026-01-31 23:16:24 +01:00
|
|
|
# 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**: `åben` or `lukket`
|
|
|
|
|
- No workflow embedded in status
|
|
|
|
|
- All workflow managed via tags
|
|
|
|
|
|
|
|
|
|
### 2. Tags (sag_tags)
|
|
|
|
|
- Represent work to be done
|
|
|
|
|
- Have state: `open` or `closed`
|
|
|
|
|
- **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
|
2026-01-29 23:07:33 +01:00
|
|
|
|
|
|
|
|
## Database Schema
|
|
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
### sag_sager (Cases)
|
|
|
|
|
- `id` SERIAL PRIMARY KEY
|
|
|
|
|
- `titel` VARCHAR(255) NOT NULL
|
|
|
|
|
- `beskrivelse` TEXT
|
|
|
|
|
- `template_key` VARCHAR(100) - used only at creation
|
|
|
|
|
- `status` VARCHAR(50) CHECK (status IN ('åben', 'lukket'))
|
|
|
|
|
- `customer_id` INT - links to customers table
|
|
|
|
|
- `ansvarlig_bruger_id` INT
|
|
|
|
|
- `created_by_user_id` INT
|
|
|
|
|
- `deadline` TIMESTAMP
|
|
|
|
|
- `created_at` TIMESTAMP DEFAULT NOW()
|
|
|
|
|
- `updated_at` TIMESTAMP DEFAULT NOW()
|
|
|
|
|
- `deleted_at` TIMESTAMP
|
2026-01-29 23:07:33 +01:00
|
|
|
|
|
|
|
|
### sag_tags (Tags)
|
2026-01-31 23:16:24 +01:00
|
|
|
- `id` SERIAL PRIMARY KEY
|
|
|
|
|
- `sag_id` INT NOT NULL REFERENCES sag_sager(id)
|
|
|
|
|
- `tag_navn` VARCHAR(100) NOT NULL
|
|
|
|
|
- `state` VARCHAR(20) CHECK (state IN ('open', 'closed'))
|
|
|
|
|
- `closed_at` TIMESTAMP
|
|
|
|
|
- `created_at` TIMESTAMP DEFAULT NOW()
|
|
|
|
|
- `deleted_at` TIMESTAMP
|
|
|
|
|
|
|
|
|
|
### sag_relationer (Relations)
|
|
|
|
|
- `id` SERIAL PRIMARY KEY
|
|
|
|
|
- `kilde_sag_id` INT NOT NULL REFERENCES sag_sager(id)
|
|
|
|
|
- `målsag_id` INT NOT NULL REFERENCES sag_sager(id)
|
|
|
|
|
- `relationstype` VARCHAR(50) NOT NULL
|
|
|
|
|
- `created_at` TIMESTAMP DEFAULT NOW()
|
|
|
|
|
- `deleted_at` TIMESTAMP
|
|
|
|
|
- `CHECK (kilde_sag_id != målsag_id)`
|
|
|
|
|
|
|
|
|
|
### sag_kontakter (Contact Links)
|
|
|
|
|
- `id` SERIAL PRIMARY KEY
|
|
|
|
|
- `sag_id` INT NOT NULL REFERENCES sag_sager(id)
|
|
|
|
|
- `contact_id` INT NOT NULL REFERENCES contacts(id)
|
|
|
|
|
- `role` VARCHAR(50)
|
|
|
|
|
- `created_at` TIMESTAMP DEFAULT NOW()
|
|
|
|
|
- `deleted_at` TIMESTAMP
|
|
|
|
|
- UNIQUE(sag_id, contact_id)
|
|
|
|
|
|
|
|
|
|
### sag_kunder (Customer Links)
|
|
|
|
|
- `id` SERIAL PRIMARY KEY
|
|
|
|
|
- `sag_id` INT NOT NULL REFERENCES sag_sager(id)
|
|
|
|
|
- `customer_id` INT NOT NULL REFERENCES customers(id)
|
|
|
|
|
- `role` VARCHAR(50)
|
|
|
|
|
- `created_at` TIMESTAMP DEFAULT NOW()
|
|
|
|
|
- `deleted_at` TIMESTAMP
|
|
|
|
|
- UNIQUE(sag_id, customer_id)
|
2026-01-29 23:07:33 +01:00
|
|
|
|
|
|
|
|
## API Endpoints
|
|
|
|
|
|
|
|
|
|
### Cases CRUD
|
2026-01-31 23:16:24 +01:00
|
|
|
- `GET /api/v1/cases` - List all cases
|
|
|
|
|
- `POST /api/v1/cases` - Create case
|
|
|
|
|
- `GET /api/v1/cases/{id}` - Get case
|
|
|
|
|
- `PATCH /api/v1/cases/{id}` - Update case
|
|
|
|
|
- `DELETE /api/v1/cases/{id}` - Soft-delete case
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
### Tags
|
|
|
|
|
- `GET /api/v1/cases/{id}/tags` - List tags
|
|
|
|
|
- `POST /api/v1/cases/{id}/tags` - Add tag
|
|
|
|
|
- `PATCH /api/v1/cases/{id}/tags/{tag_id}/state` - Toggle tag state
|
|
|
|
|
- `DELETE /api/v1/cases/{id}/tags/{tag_id}` - Soft-delete tag
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
### Relations
|
|
|
|
|
- `GET /api/v1/cases/{id}/relations` - List relations
|
|
|
|
|
- `POST /api/v1/cases/{id}/relations` - Create relation
|
|
|
|
|
- `DELETE /api/v1/cases/{id}/relations/{rel_id}` - Soft-delete relation
|
|
|
|
|
|
|
|
|
|
### Contacts & Customers
|
|
|
|
|
- `GET /api/v1/cases/{id}/contacts` - List linked contacts
|
|
|
|
|
- `POST /api/v1/cases/{id}/contacts` - Link contact
|
|
|
|
|
- `DELETE /api/v1/cases/{id}/contacts/{contact_id}` - Unlink contact
|
|
|
|
|
- `GET /api/v1/cases/{id}/customers` - List linked customers
|
|
|
|
|
- `POST /api/v1/cases/{id}/customers` - Link customer
|
|
|
|
|
- `DELETE /api/v1/cases/{id}/customers/{customer_id}` - Unlink customer
|
|
|
|
|
|
|
|
|
|
### Search
|
|
|
|
|
- `GET /api/v1/search/cases?q={query}` - Search cases
|
|
|
|
|
- `GET /api/v1/search/contacts?q={query}` - Search contacts
|
|
|
|
|
- `GET /api/v1/search/customers?q={query}` - Search customers
|
|
|
|
|
|
|
|
|
|
### Bulk Operations
|
|
|
|
|
- `POST /api/v1/cases/bulk` - Bulk actions (close, add tag)
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
## Frontend Routes
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
- `/cases` - List all cases
|
|
|
|
|
- `/cases/new` - Create new case
|
|
|
|
|
- `/cases/{id}` - View case details
|
|
|
|
|
- `/cases/{id}/edit` - Edit case
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
## Usage Examples
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
### Create a Case
|
|
|
|
|
```python
|
|
|
|
|
import requests
|
|
|
|
|
response = requests.post('http://localhost:8001/api/v1/cases', json={
|
|
|
|
|
'titel': 'New Project',
|
|
|
|
|
'beskrivelse': 'Project description',
|
|
|
|
|
'status': 'åben',
|
|
|
|
|
'customer_id': 123
|
|
|
|
|
})
|
2026-01-29 23:07:33 +01:00
|
|
|
```
|
|
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
### Add Tag to Case
|
|
|
|
|
```python
|
|
|
|
|
response = requests.post('http://localhost:8001/api/v1/cases/1/tags', json={
|
|
|
|
|
'tag_navn': 'urgent'
|
|
|
|
|
})
|
2026-01-29 23:07:33 +01:00
|
|
|
```
|
|
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
### Close a Tag (Mark Work Complete)
|
|
|
|
|
```python
|
|
|
|
|
response = requests.patch('http://localhost:8001/api/v1/cases/1/tags/5/state', json={
|
|
|
|
|
'state': 'closed'
|
|
|
|
|
})
|
2026-01-29 23:07:33 +01:00
|
|
|
```
|
|
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
### Link Related Case
|
|
|
|
|
```python
|
|
|
|
|
response = requests.post('http://localhost:8001/api/v1/cases/1/relations', json={
|
|
|
|
|
'målsag_id': 42,
|
|
|
|
|
'relationstype': 'blokkerer'
|
|
|
|
|
})
|
2026-01-29 23:07:33 +01:00
|
|
|
```
|
|
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
## Relation Types
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
- **relateret** - General relation
|
|
|
|
|
- **afhænger af** - This case depends on target
|
|
|
|
|
- **blokkerer** - This case blocks target
|
|
|
|
|
- **duplikat** - This case duplicates target
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
## Orders Integration
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
Orders are **independent entities** but gain meaning through relations to cases.
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
When creating an Order from a Case:
|
|
|
|
|
1. Create the Order independently
|
|
|
|
|
2. Create a relation: Case → Order
|
|
|
|
|
3. Use relationstype: `ordre_oprettet` or similar
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
Orders **do not replace cases** - they are transactional satellites.
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
## Design Philosophy
|
|
|
|
|
|
|
|
|
|
> "If you think you need a new table or workflow engine, you're probably wrong. Use relations and tags instead."
|
2026-01-29 23:07:33 +01:00
|
|
|
|
2026-01-31 23:16:24 +01:00
|
|
|
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
|