@opencxh/domain 1.161.0 → 1.162.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,17 @@
1
- export type CustomFieldType = "text" | "select";
1
+ /**
2
+ * De soorten aangepaste velden.
3
+ *
4
+ * Begon als `text | select`, want dat was alles wat een gesprek nodig had. Werkitems vragen
5
+ * er meer: een dealwaarde moet als bedrag getoond worden, een doorlooptijd als duur, en een
6
+ * klant als verwijzing naar het bedrijvenregister in plaats van als vrije tekst.
7
+ *
8
+ * Bewust **geen** `multi_select`: {@link CustomFieldDef.multiValued} bestaat al, wordt al
9
+ * opgeslagen en betekent precies dat. Twee manieren om één ding te zeggen levert een
10
+ * renderer op met een tak die niemand onderhoudt.
11
+ */
12
+ export type CustomFieldType = "text" | "textarea" | "number" | "currency" | "date" | "duration" | "select" | "user" | "checkbox"
13
+ /** Verwijst naar een resource elders in het platform; zie {@link CustomFieldDef.refKinds}. */
14
+ | "resource_ref";
2
15
  /** A selectable option for a `select` field. `label` falls back to `value` when absent. */
3
16
  export interface CustomFieldOption {
4
17
  value: string;
@@ -21,10 +34,24 @@ export interface CustomFieldDef {
21
34
  options?: CustomFieldOption[];
22
35
  /** `select` only: multi-select — the stored value becomes a string[]. */
23
36
  multiValued?: boolean;
24
- /** Resource kinds this field applies to, e.g. ["interaction"]. */
37
+ /** Resource kinds this field applies to, e.g. ["interaction"], ["work_item"]. */
25
38
  appliesTo: string[];
26
39
  /** Empty/absent = all teams; otherwise only shown when the resource belongs to one of these teams. */
27
40
  teamIds?: string[];
41
+ /**
42
+ * Voor `resource_ref`: welke scope-soorten gekozen mogen worden (`"company"`,
43
+ * `"contact"`, `"work_item"`). Leeg = alles wat de kiezer aanbiedt.
44
+ */
45
+ refKinds?: string[];
46
+ /**
47
+ * Welke work-projecten dit veld aanzetten. Leeg/afwezig = elk project, net als bij
48
+ * {@link CustomFieldDef.teamIds}.
49
+ *
50
+ * Bestaat omdat een velddefinitie platformbreed is maar een veldenset dat niet is: twee
51
+ * projecten mogen allebei een "Bedrag" kennen met een eigen betekenis. Additief, dus
52
+ * bestaande definities houden een lege lijst en blijven overal gelden.
53
+ */
54
+ projectIds?: string[];
28
55
  /**
29
56
  * When true, this field becomes an analytics dimension `comms.cf.<key>`: every conversation-level
30
57
  * metric can be grouped/filtered by its value. Opt-in to avoid turning free-text fields into
@@ -1,2 +1,3 @@
1
1
  export * from './duration';
2
2
  export * from './types';
3
+ export * from './work-type';
@@ -78,7 +78,14 @@ export interface TimeEntry {
78
78
  export interface WorkType {
79
79
  id: string;
80
80
  organizationId: string;
81
- /** Stabiele sleutel die op de urenregel belandt. */
81
+ /**
82
+ * Stabiele sleutel die op de urenregel belandt.
83
+ *
84
+ * Uniek binnen de organisatie en niet binnen de scope, hoewel de soort een team kan
85
+ * toebehoren: `TimeEntry.workType` bewaart alléén deze sleutel en de rapportage groepeert
86
+ * erop. Twee teams met elk een eigen `installatie` zouden daar als één regel uitkomen, met
87
+ * twee betekenissen erin.
88
+ */
82
89
  key: string;
83
90
  label: string;
84
91
  /** Standaardwaarde voor `TimeEntry.billable`; per regel te overrulen. */
@@ -87,9 +94,37 @@ export interface WorkType {
87
94
  reportable?: boolean;
88
95
  order?: number;
89
96
  archived?: boolean;
97
+ /**
98
+ * Van wie deze soort is: de hele organisatie of één team. Bepaalt wie hem in zijn
99
+ * urenpaneel ziet en wie hem mag beheren.
100
+ *
101
+ * Geen `personal`, om dezelfde reden als bij `Topic`: een soort die maar één mens kent,
102
+ * maakt het geboekte uur onleesbaar voor de collega die de week overneemt. Afwezig telt als
103
+ * org-breed — dat is wat elke soort vóór dit veld feitelijk was.
104
+ */
105
+ ownerScope?: Extract<OwnerScope, {
106
+ kind: "org";
107
+ } | {
108
+ kind: "team";
109
+ }>;
90
110
  createdAt?: number;
91
111
  updatedAt?: number;
92
112
  }
113
+ /**
114
+ * Wat het beheerscherm naar de server stuurt.
115
+ *
116
+ * Zonder `id` is het een nieuwe soort. `key` staat er met opzet niet in: die leidt de server af
117
+ * uit de naam en ligt daarna vast, omdat geboekte uren ernaar verwijzen.
118
+ */
119
+ export interface SaveWorkTypeRequest {
120
+ id?: string;
121
+ label: string;
122
+ defaultBillable?: boolean;
123
+ reportable?: boolean;
124
+ order?: number;
125
+ archived?: boolean;
126
+ ownerScope?: WorkType["ownerScope"];
127
+ }
93
128
  /** Een kandidaat waarop de gebruiker kan klokken, aangeboden in de leegstaat van het paneel. */
94
129
  export interface TimeTarget {
95
130
  scopeKey?: string;
@@ -0,0 +1,44 @@
1
+ import { WorkType } from './types';
2
+ /**
3
+ * De sleutel die bij een naam hoort: kleine letters, streepjes, geen accenten.
4
+ *
5
+ * In `domain` en niet in de app-server omdat het beheerscherm hem al tijdens het typen laat
6
+ * zien — wat de gebruiker daar ziet moet exact zijn wat de server straks wegschrijft, anders
7
+ * belooft het formulier `installatie-op-locatie` en staat er `installatieoplocatie` in de
8
+ * rapportage.
9
+ *
10
+ * `normalize("NFD")` plus het weghalen van de combineertekens, zodat "Béta" `beta` wordt en niet
11
+ * `b-ta`: een leesbare sleutel is het halve doel.
12
+ */
13
+ export declare function workTypeKey(label: string): string;
14
+ /**
15
+ * Dezelfde sleutel, maar gegarandeerd nog vrij binnen de organisatie.
16
+ *
17
+ * Botsingen zijn echt: de sleutel is org-uniek terwijl soorten per team beheerd worden, dus twee
18
+ * teams die allebei "Installatie" aanmaken komen hier uit. De tweede krijgt `installatie-2` in
19
+ * plaats van een foutmelding — een beheerder die een naam kiest, hoort niet te struikelen over
20
+ * een sleutel die hij nooit heeft gezien.
21
+ *
22
+ * `taken` bevat óók gearchiveerde sleutels: die staan nog op geboekte uren, dus hergebruiken zou
23
+ * oud werk stilzwijgend onder een nieuwe soort schuiven.
24
+ */
25
+ export declare function uniqueWorkTypeKey(label: string, taken: Iterable<string>): string;
26
+ /**
27
+ * Ziet deze gebruiker deze soort?
28
+ *
29
+ * Een soort zonder scope is org-breed: rijen zijn ouder dan het veld, en ze wegfilteren zou een
30
+ * bestaande organisatie zijn hele lijst kosten.
31
+ */
32
+ export declare function isWorkTypeVisible(workType: Pick<WorkType, "ownerScope">, teamIds: string[]): boolean;
33
+ /**
34
+ * Wat het paneel toont: niet-gearchiveerd, in de juiste volgorde.
35
+ *
36
+ * Zonder team-argument, en dat is met opzet: de server heeft de teamzichtbaarheid al afgekapt
37
+ * voordat de lijst de client bereikt (dat is een rechtenvraag). Zou de client hier nóg eens op
38
+ * `teamIds` filteren, dan moest hij het lidmaatschap van de gebruiker kennen — en met een lege
39
+ * lijst, zoals hij hem nu heeft, zou hij precies de teamsoorten wegfilteren die de server net
40
+ * heeft goedgekeurd.
41
+ */
42
+ export declare function activeWorkTypes(workTypes: WorkType[]): WorkType[];
43
+ /** De volgorde van de chips: `order` eerst, gelijke waarden alfabetisch zodat hij stabiel is. */
44
+ export declare function compareWorkTypes(a: WorkType, b: WorkType): number;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './keys';
2
+ export * from './ladder';
3
+ export * from './types';
@@ -0,0 +1,57 @@
1
+ import { WorkItem, WorkProject } from './types';
2
+ /**
3
+ * Sleutels: de scopeKeys waarmee andere apps werk aanwijzen, en de menselijke itemsleutel.
4
+ *
5
+ * Beide staan in `domain` en niet in de app, omdat het contracten zijn: de client bouwt een
6
+ * scopeKey om een timer te starten, de server autoriseert hem, en twee plekken die het
7
+ * voorvoegsel los uitschrijven lopen bij de eerste typefout stil uiteen — met "geen toegang
8
+ * tot deze scope" als gevolg in plaats van een foutmelding die zegt wat er mis is.
9
+ */
10
+ /** De scope-soorten die `apps/work` claimt via `/provider/scope/describe`. */
11
+ export declare const WORK_ITEM_SCOPE_KIND = "work_item";
12
+ export declare const WORK_PROJECT_SCOPE_KIND = "work_project";
13
+ export declare function workItemScopeKey(itemId: string): string;
14
+ export declare function workProjectScopeKey(projectId: string): string;
15
+ /**
16
+ * De menselijke sleutel: `SAL-142`, of `SAL-142-1` voor een subitem.
17
+ *
18
+ * Alleen voor items mét project — een los item heeft er geen, en dat is geen omissie maar het
19
+ * verschil tussen "werk dat een plek in een proces heeft" en "iets wat ik nog moet doen".
20
+ */
21
+ export declare function formatItemKey(projectKey: string, sequenceNumber: number, subSequence?: number): string;
22
+ /** Wat er in een `SAL-142-1` zit, of `null` als het geen itemsleutel is. */
23
+ export interface ParsedItemKey {
24
+ projectKey: string;
25
+ sequenceNumber: number;
26
+ subSequence?: number;
27
+ }
28
+ /**
29
+ * Leest een sleutel die iemand heeft ingetypt of geplakt.
30
+ *
31
+ * Hoofdletterongevoelig aan de invoerkant en genormaliseerd aan de uitvoerkant, want mensen
32
+ * typen `sal-142` in een zoekveld en verwachten hun item. Het projectvoorvoegsel is
33
+ * `[A-Z0-9]{2,8}`, wat een streepje uitsluit — anders zou `SAL-142-1` net zo goed te lezen
34
+ * zijn als project `SAL-142`, item `1`.
35
+ */
36
+ export declare function parseItemKey(input: string): ParsedItemKey | null;
37
+ /** De projectsleutel die bij een naam hoort: `"Sales pipeline"` → `"SAL"`. */
38
+ export declare function suggestProjectKey(name: string): string;
39
+ /**
40
+ * De dossiersleutels van een item: waarmee uren, geheugen en attributen het terugvinden.
41
+ *
42
+ * **Klein houden.** Dit is de set van de rij zelf en niet zijn hele omgeving — deze sleutels
43
+ * rijden mee op elke autorisatie, inclusief elke geheugen-schrijfactie, dus de kinderen van
44
+ * dit item horen er niet in. Dezelfde afweging die `companyDossierKeys` maakt tegenover
45
+ * `keySetForCompany` in crm.
46
+ *
47
+ * De harde bovengrens is er voor het pathologische geval: een item met tweehonderd partijen
48
+ * mag geen array van tweehonderd sleutels op elke autorisatie leggen.
49
+ */
50
+ export declare function itemDossierKeys(item: Pick<WorkItem, "id" | "projectId" | "ancestorKeys" | "partyKeys">, limit?: number): string[];
51
+ /**
52
+ * De dossiersleutels van een project: alleen zichzelf.
53
+ *
54
+ * Een project heeft onbegrensd veel items, dus zijn kinderen meegeven zou een onbegrensd
55
+ * array op elke autorisatie leggen. Wie de items wil, vraagt ernaar.
56
+ */
57
+ export declare function projectDossierKeys(project: Pick<WorkProject, "id">): string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ import { WorkProject, WorkStatus, WorkStatusCategory } from './types';
2
+ /**
3
+ * De statusladder — de enige plek die weet of een item een project heeft.
4
+ *
5
+ * `WorkItem.projectId` is optioneel, en zonder discipline betekent dat een `if (projectId)`
6
+ * in elke lijst, elke teller en elke statuskiezer. Alles wat "welke statussen kan dit item
7
+ * hebben" of "is dit open" vraagt gaat daarom door {@link ladderFor} en {@link categoryOf},
8
+ * en nergens anders.
9
+ */
10
+ /**
11
+ * De vaste ladder voor werk zónder project.
12
+ *
13
+ * Deze drie sleutels zijn **gereserveerd**: een project mag ze niet claimen, want dan zou
14
+ * dezelfde sleutel in twee ladders een andere categorie kunnen hebben en werd de
15
+ * org-brede "wat staat er open"-query stil onjuist. `validateStatuses` bewaakt dat.
16
+ *
17
+ * De labels zijn **i18n-sleutels, geen tekst** — een letterlijke string hier zou de taal op
18
+ * moduleniveau vastzetten voor elke consument, en dat is precies wat de i18n-regel in
19
+ * `DESIGN_SYSTEM.md` §11 verbiedt. Ze staan zonder app-voorvoegsel omdat de app-sdk dat er
20
+ * bij het opzoeken zelf voor zet; een `work:`-voorvoegsel hier zou in de eigen app
21
+ * verdubbelen tot `work:work:status_open`.
22
+ *
23
+ * Een projectstatus heeft géén sleutel maar een naam die een beheerder heeft ingetypt. Alleen
24
+ * deze drie zijn vertaalbaar, want alleen deze zijn van ons.
25
+ */
26
+ export declare const LOOSE_STATUSES: readonly WorkStatus[];
27
+ /** De sleutels die {@link LOOSE_STATUSES} bezet houdt. */
28
+ export declare const RESERVED_STATUS_KEYS: readonly string[];
29
+ /**
30
+ * De ladder die voor dit item geldt.
31
+ *
32
+ * Een project zónder statussen valt terug op de vaste ladder in plaats van een lege lijst op
33
+ * te leveren: een statuskiezer met nul opties is een doodlopende weg, en een half aangemaakt
34
+ * project hoort niet onbruikbaar te zijn.
35
+ */
36
+ export declare function ladderFor(project?: WorkProject | null): readonly WorkStatus[];
37
+ /**
38
+ * De categorie van een statussleutel binnen een ladder.
39
+ *
40
+ * Valt terug op `"todo"` bij een onbekende sleutel, en dat is de veilige kant: een item dat
41
+ * door een verwijderde status wees zou anders als afgerond uit elke telling verdwijnen. Beter
42
+ * zichtbaar op de verkeerde plek dan onzichtbaar.
43
+ */
44
+ export declare function categoryOf(statusKey: string, ladder: readonly WorkStatus[]): WorkStatusCategory;
45
+ /** De status zelf, als de ladder hem kent. */
46
+ export declare function statusIn(statusKey: string, ladder: readonly WorkStatus[]): WorkStatus | undefined;
47
+ /** Waar een nieuw item begint: de expliciete keuze van het project, anders de eerste stap. */
48
+ export declare function defaultStatusKey(project?: WorkProject | null): string;
49
+ /** `order` eerst, gelijke waarden op label zodat de volgorde stabiel is. */
50
+ export declare function sortStatuses(ladder: readonly WorkStatus[]): WorkStatus[];
51
+ /**
52
+ * Is dit item afgerond? De enige juiste manier om die vraag te stellen.
53
+ *
54
+ * `isWorkClosed` en niet `isClosed`, want `entities/interaction` exporteert die naam al voor
55
+ * een gesprek. Twee `isClosed`en in één barrel is precies de verwarring waar dit veld niet
56
+ * op zit te wachten.
57
+ */
58
+ export declare function isWorkClosed(statusKey: string, ladder: readonly WorkStatus[]): boolean;
59
+ /**
60
+ * De statussleutel die bij een naam hoort: kleine letters, streepjes, geen accenten.
61
+ *
62
+ * Dezelfde vorm en dezelfde reden als `workTypeKey` bij de werksoorten: het beheerscherm
63
+ * toont hem al tijdens het typen, dus wat de gebruiker daar ziet moet precies zijn wat de
64
+ * server wegschrijft. `normalize("NFD")` plus het weghalen van de combineertekens, zodat
65
+ * "Béta" `beta` wordt en niet `b-ta`.
66
+ */
67
+ export declare function workStatusKey(label: string): string;
68
+ /**
69
+ * Dezelfde sleutel, gegarandeerd nog vrij binnen de organisatie.
70
+ *
71
+ * Botsingen zijn hier de regel en niet de uitzondering: sleutels zijn org-uniek terwijl
72
+ * statussen per project beheerd worden, dus twee projecten die allebei een "Review" aanmaken
73
+ * komen hier uit. De tweede krijgt `review-2` in plaats van een foutmelding — een beheerder
74
+ * die een naam kiest hoort niet te struikelen over een sleutel die hij nooit heeft gezien.
75
+ *
76
+ * `taken` moet óók de sleutels van verwijderde statussen bevatten zolang er items naar
77
+ * wijzen, en altijd {@link RESERVED_STATUS_KEYS}.
78
+ */
79
+ export declare function uniqueWorkStatusKey(label: string, taken: Iterable<string>): string;
80
+ /** Wat er mis is met een voorgestelde ladder, als leesbare redenen. */
81
+ export interface StatusLadderProblem {
82
+ /** Index in de aangeleverde lijst, of `-1` als het de lijst als geheel betreft. */
83
+ index: number;
84
+ reason: "empty" | "duplicate_key" | "reserved_key" | "missing_key" | "no_open" | "no_done";
85
+ key?: string;
86
+ }
87
+ /**
88
+ * Valideert een ladder vóór hij wordt opgeslagen.
89
+ *
90
+ * Bestaat omdat de store hier niets bewaakt: `statuses` is één ingebed array, dus uniciteit
91
+ * en volledigheid zijn de verantwoordelijkheid van de schrijver. Puur en zonder I/O, zodat
92
+ * het beheerscherm dezelfde regels kan tonen als de server afdwingt.
93
+ *
94
+ * De twee laatste regels zijn geen smaak: een ladder zonder open-categorie maakt elk nieuw
95
+ * item meteen afgerond, en een ladder zonder `done` maakt afronden onmogelijk.
96
+ */
97
+ export declare function validateStatuses(statuses: readonly WorkStatus[]): StatusLadderProblem[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,255 @@
1
+ import { OwnerScope } from '../contact/types';
2
+ /**
3
+ * Work management — één entiteit voor alles wat *werk* is.
4
+ *
5
+ * Taak, subtaak, zaak, project, fase, deal en campagne zijn hier rijen in hetzelfde model.
6
+ * Wat ze onderscheidt is configuratie, geen code: het **project** draagt de workflow, de
7
+ * veldselectie en de toegang, en `typeKey` is niet meer dan een icoon met een etiket.
8
+ *
9
+ * De regel waarmee dit model is afgebakend, in drie vragen:
10
+ *
11
+ * 1. *Wie bepaalt de statusovergang?* Onze gebruikers → een `WorkItem`. Een extern systeem
12
+ * (Shopify, de boekhouding) → gespiegeld door de bron-app, hier hooguit een `WorkLink`.
13
+ * 2. *Eigen levensduur, of hangt het ergens aan?* Eigen → een item. Hangt eraan (uren,
14
+ * notities, attributen, geheugen) → een annotatie op een `scopeKey`, nooit een rij hier.
15
+ * 3. *Ander gedrag, of andere woorden?* Alleen andere woorden → een `WorkProject` met andere
16
+ * statussen. Pas bij ander gedrag komt er code bij.
17
+ *
18
+ * Test 3 is waarom "Dossier", "Project" en "Deal" geen drie entiteiten zijn.
19
+ */
20
+ /**
21
+ * De drie categorieën waarin elke status valt.
22
+ *
23
+ * Vast, terwijl de statusnaam per project vrij is ("Offerte", "Wacht op cliënt"). Dit is de
24
+ * vraag die élke consument stelt — de lijst, de teller, de rapportage — en zou die de
25
+ * projectconfiguratie moeten lezen om hem te beantwoorden, dan kon geen enkele query het
26
+ * geïndexeerd doen.
27
+ */
28
+ export type WorkStatusCategory = "todo" | "in_progress" | "done";
29
+ /**
30
+ * Eén stap in de workflow van een project.
31
+ *
32
+ * `key` is **org-uniek en niet project-uniek**, en dat is een bewuste last op het
33
+ * beheerscherm in ruil voor iets belangrijks: het item draagt alleen `statusKey`, dus
34
+ * "al mijn open werk over alle projecten heen" is één `{ statusKey: anyOf(openKeys) }`.
35
+ * Zouden twee projecten allebei een `done` mogen hebben met een andere categorie, dan gaf
36
+ * diezelfde query stilzwijgend het verkeerde antwoord. De sleutel wordt daarom net als bij
37
+ * de werksoorten uit het label geslugd en bij botsing opgehoogd.
38
+ *
39
+ * `label` is vrij te hernoemen; `key` ligt vast zodra er items op staan.
40
+ */
41
+ export interface WorkStatus {
42
+ key: string;
43
+ label: string;
44
+ category: WorkStatusCategory;
45
+ order: number;
46
+ /** Optionele accentkleur voor de bolletjes in de lijst en het beheerscherm. */
47
+ color?: string;
48
+ }
49
+ /**
50
+ * Een afloopreden: hóé iets eindigde, los van wáár het staat.
51
+ *
52
+ * Bewust een vrij label zonder vaste betekenis en zonder uitkomst-vlag. Een advocatenkantoor
53
+ * onderscheidt "geschikt" van "vonnis", een salesteam "gewonnen" van "verloren", en geen van
54
+ * beide past in een enum die wij bedenken. De prijs staat in het plan: niets kan uitrekenen
55
+ * hoeveel er goed afliep. Komt die vraag, dan is een `positive?: boolean` hier de plek.
56
+ */
57
+ export interface WorkResolution {
58
+ key: string;
59
+ label: string;
60
+ order: number;
61
+ }
62
+ /**
63
+ * Een project: de drager van een werkproces.
64
+ *
65
+ * Dit is waar Jira zijn schemes heeft en wij niet. Workflow, veldselectie en toegang hangen
66
+ * rechtstreeks aan het project in plaats van aan drie koppelbare configuratieobjecten — dat
67
+ * scheelt de drie schermen die je bij Jira langs moet om één status te veranderen.
68
+ */
69
+ export interface WorkProject {
70
+ id: string;
71
+ organizationId: string;
72
+ /**
73
+ * Org-uniek, hoofdletters: `"SAL"`. Vormt het voorvoegsel van elke itemsleutel
74
+ * (`SAL-142`), en ligt daarom vast zodra er items zijn — hem wijzigen zou elke sleutel
75
+ * hernoemen, inclusief de exemplaren die mensen in een mail hebben geplakt.
76
+ */
77
+ key: string;
78
+ name: string;
79
+ description?: string;
80
+ /** Lucide-icoonnaam. */
81
+ icon?: string;
82
+ color?: string;
83
+ ownerScope: OwnerScope;
84
+ /** Leden bovenop `ownerScope` — wie er nog meer bij mag. */
85
+ memberUserIds?: string[];
86
+ memberTeamIds?: string[];
87
+ /**
88
+ * De workflow, ingebed in plaats van een eigen entiteit.
89
+ *
90
+ * Drie redenen. Niets bevraagt ooit een losse statusrij: elke itemvraag wordt beantwoord
91
+ * door de platte `statusKey` op het item. De leesvorm is altijd "de hele ladder tegelijk",
92
+ * want een lijst rendert alle labels, kleuren en volgordes. En het beheerscherm bewerkt de
93
+ * ladder als één herordening — deze store kent geen transactie, dus als losse rijen zou
94
+ * "sleep status 3 boven status 1" N updates worden die half kunnen landen en twee
95
+ * statussen op dezelfde `order` achterlaten. Als array is het één `$set`.
96
+ *
97
+ * Wat het kost: uniciteit van `key` wordt niet door de store bewaakt maar door
98
+ * `validateStatuses` in de app.
99
+ */
100
+ statuses: WorkStatus[];
101
+ /** Afloopredenen, vrij per project. Leeg = afronden vraagt geen reden. */
102
+ resolutions?: WorkResolution[];
103
+ /** Welke itemtypes dit project aanbiedt. Leeg = alle. */
104
+ typeKeys?: string[];
105
+ /** Sleutels van de `CustomFieldDef`s die op dit project aan staan, in weergavevolgorde. */
106
+ fieldKeys?: string[];
107
+ /** Waar een nieuw item begint. Afwezig = de eerste status in `order`. */
108
+ defaultStatusKey?: string;
109
+ /**
110
+ * Duurzaam ankerpunt voor de sleutelteller.
111
+ *
112
+ * De teller zelf leeft in `Bridge.kv`, maar kv is een cache: hij kan koud starten of
113
+ * geëvicteerd worden. Dit veld en de `(organizationId, projectId, sequenceNumber)`-index
114
+ * zijn samen de weg terug, zodat een lege kv geen sleutels hergebruikt.
115
+ */
116
+ lastSequence?: number;
117
+ archived?: boolean;
118
+ order?: number;
119
+ createdBy: string;
120
+ createdAt?: number;
121
+ updatedAt?: number;
122
+ }
123
+ export type WorkPriority = "low" | "normal" | "high" | "urgent";
124
+ /** Waar een item vandaan komt — een mens of iets automatisch. */
125
+ export interface WorkItemSource {
126
+ initiator: "user" | "system";
127
+ /** Vrije reden bij `system` ("missed_call", "stale_interaction"). */
128
+ systemReason?: string;
129
+ }
130
+ /**
131
+ * Een werkitem.
132
+ *
133
+ * Let op wat hier **niet** staat: geen `interactionId`, geen `contactId`, geen `companyId`.
134
+ * Verbanden lopen via {@link WorkLink} plus de platgeslagen {@link WorkItem.keys}, zodat "de
135
+ * items op dit gesprek" één geïndexeerde `$in` blijft die combineerbaar is met een
136
+ * statusfilter — wat een omweg langs de koppeltabel niet is.
137
+ *
138
+ * En geen `value`/`currency`: bedragen zijn aangepaste velden in de generieke
139
+ * attribuutstore. Dat maakt sorteren erop een geheugenoperatie; zie het plan.
140
+ */
141
+ export interface WorkItem {
142
+ id: string;
143
+ organizationId: string;
144
+ /**
145
+ * Afwezig = een **los item**: geen sleutel, geen projectvelden, en de vaste ladder uit
146
+ * {@link LOOSE_STATUSES}. Dat is wat een losse taak is ("terugbellen", een opvolging op
147
+ * een gesprek), en het bestaat vanaf dag één zodat werk dat later uit een andere app
148
+ * hierheen komt geen modelwijziging vraagt.
149
+ */
150
+ projectId?: string;
151
+ /** `SAL-142` / `SAL-142-1`. Afwezig bij een los item. Org-uniek waar aanwezig. */
152
+ key?: string;
153
+ sequenceNumber?: number;
154
+ /** Volgnummer binnen de ouder — het `-1` in `SAL-142-1`. */
155
+ subSequence?: number;
156
+ /** Lichte classificatie: icoon en welke velden zinvol zijn. Nooit een workflow. */
157
+ typeKey: string;
158
+ ownerScope: OwnerScope;
159
+ title: string;
160
+ description?: string;
161
+ /**
162
+ * Een {@link WorkStatus.key} — die van het project, of uit de vaste ladder. Omdat de
163
+ * sleutels org-uniek zijn is de categorie er altijd eenduidig bij te zoeken; daarom
164
+ * staat die hier níet als tweede kolom.
165
+ */
166
+ statusKey: string;
167
+ /** {@link WorkResolution.key}. Alleen zinvol als de status in categorie `done` valt. */
168
+ resolution?: string;
169
+ parentId?: string;
170
+ /**
171
+ * De scopeKeys van álle voorouders, platgeslagen op schrijfmoment
172
+ * (`["work_item:program-1", "work_item:project-7"]`, van boven naar beneden).
173
+ *
174
+ * Dít maakt onbeperkte diepte betaalbaar: "alles onder dit project" is één geïndexeerde
175
+ * `$in` in plaats van een recursieve traversal, en die kan deze store niet. De prijs zit
176
+ * aan de schrijfkant — een item verslepen betekent de `ancestorKeys` van zijn hele
177
+ * subtree herschrijven, en dat is een job, geen request.
178
+ */
179
+ ancestorKeys?: string[];
180
+ /** UserIds die dit doen. Leeg = de pool van het team. */
181
+ assignees?: string[];
182
+ /** Wie het inbracht, los van wie het doet. */
183
+ reporterId?: string;
184
+ watchers?: string[];
185
+ priority?: WorkPriority;
186
+ labels?: string[];
187
+ /**
188
+ * De partijen, plat en zonder rol: `["company:123", "contact:456"]`.
189
+ *
190
+ * Afgeleid uit de {@link WorkLink}s op schrijfmoment. Twee vormen naast elkaar omdat ze
191
+ * twee vragen beantwoorden: de link weet *in welke hoedanigheid* (cliënt, wederpartij),
192
+ * deze kolom beantwoordt "alle items waar dit bedrijf in zit" in één indexhit — wat over
193
+ * een koppeltabel een tweede ronde zou kosten.
194
+ */
195
+ partyKeys: string[];
196
+ /**
197
+ * Dossiersleutels — de `keys`-conventie uit `platform/scope.ts`. Eigen sleutel,
198
+ * projectsleutel, voorouders en partijen. Hiermee vinden uren, geheugen en attributen
199
+ * dit item terug.
200
+ */
201
+ keys: string[];
202
+ /** Verwijzingen naar externe systemen (`shopify_order:8842`). Nooit een kopie. */
203
+ externalIds?: string[];
204
+ /** Epoch ms. */
205
+ startDate?: number;
206
+ dueDate?: number;
207
+ /** Gezet zodra de status naar categorie `done` gaat, gewist als hij terugkomt. */
208
+ closedAt?: number;
209
+ estimateSeconds?: number;
210
+ /** Standaard voor uren die hierop geboekt worden; de urenregel mag afwijken. */
211
+ billable?: boolean;
212
+ source: WorkItemSource;
213
+ createdBy: string;
214
+ createdAt?: number;
215
+ updatedAt?: number;
216
+ }
217
+ /**
218
+ * Een getypeerde koppeling tussen een werkitem en wat dan ook.
219
+ *
220
+ * **Eén hop, nooit getraverseerd.** Dat is de afspraak die deze tabel naast de
221
+ * `keys`-conventie laat bestaan in plaats van ermee te concurreren: context en lidmaatschap
222
+ * lopen via `keys` (platgeslagen bij schrijven, één indexhit), en dit is de expliciete,
223
+ * laag-volume, één-niveau-diepe relatie tussen twee dingen. Een link-tabel die alsnog de
224
+ * contextvraag beantwoordt is het begin van de grafiek die hier bewust niet gebouwd is —
225
+ * deze store kan niet recursief traverseren.
226
+ */
227
+ export interface WorkLink {
228
+ id: string;
229
+ organizationId: string;
230
+ /** Altijd `work_item:<id>` op de heenrij. */
231
+ fromKey: string;
232
+ /** Elke scopeKey: een ander item, een gesprek, een contact, een artefact. */
233
+ toKey: string;
234
+ /**
235
+ * De relatie én de partijrol in één vocabulaire: `client` · `opposing` · `context` ·
236
+ * `blocks` · `blocked_by` · `relates` · `duplicates` · `attachment`.
237
+ *
238
+ * Eén veld en geen aparte rol-kolom, want het zijn dezelfde soort uitspraak: "dit bedrijf
239
+ * is de cliënt" en "dit item blokkeert dat item" zijn allebei een benoemde pijl.
240
+ */
241
+ type: string;
242
+ /**
243
+ * Gedeeld door de heen- en terugrij.
244
+ *
245
+ * Elke koppeling wordt als **twee** rijen geschreven, zodat een lookup één geïndexeerde
246
+ * query op `fromKey` is in plaats van een `$or` over twee kolommen — en zodat "welk werk
247
+ * hangt aan `interaction:123`" dezelfde query is als "wat hangt aan dit item". Dit veld
248
+ * houdt ontkoppelen daarmee één handeling.
249
+ */
250
+ pairId: string;
251
+ /** Waar de terugrij zich mee bekendmaakt, zodat de UI "wordt geblokkeerd door" kan tonen. */
252
+ inverse?: boolean;
253
+ createdBy: string;
254
+ createdAt?: number;
255
+ }