@opencxh/domain 1.160.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.
@@ -106,22 +106,27 @@ export interface Assignment {
106
106
  /**
107
107
  * Wekt deze activity een opdracht die op een antwoord wacht?
108
108
  *
109
- * **Twee bronnen, twee regels, en dat verschil is het hele punt.**
109
+ * **Vier regels, van hard naar zacht.**
110
110
  *
111
- * - **De klant antwoordt** (`inbound`): dat wekt altijd. Een klant kan niemand @-noemen, en zijn
112
- * antwoord is per definitie het antwoord waar de opdracht op wachtte.
113
- * - **Een collega schrijft een interne notitie**: die wekt **alleen als de agent genoemd is**.
111
+ * 1. **De klant antwoordt** (`inbound`): wekt altijd. Een klant kan niemand @-noemen, en zijn
112
+ * antwoord is per definitie het antwoord waar de opdracht op wachtte.
113
+ * 2. **De agent zelf schreef dit**: nooit. Zonder deze poort is de vraag die de agent stelt het
114
+ * signaal waarop hij wakker wordt — een lus die pas bij het beurtplafond stopt.
115
+ * 3. **De agent is genoemd**: wekt, ongeacht waarop hij wacht. Een vermelding is de expliciete
116
+ * vraag die de engine zelf niet kan beantwoorden: *is dit aan míjn agent gericht?*
117
+ * 4. **Een interne notitie terwijl hij op een antwoord wacht** (`waitingOn === "reply"`): wekt.
114
118
  *
115
- * Die tweede regel is er omdat "elke interne notitie wekt" te grof is: twee collega's die in het
116
- * dossier overleggen zouden dan elk een modelbeurt kosten, over iets waar de agent niet bij
117
- * gevraagd was — en dat vreet het beurtplafond op aan gesprek dat niet voor hem bedoeld was.
119
+ * Regel 4 is nieuw en vervangt "alleen als hij genoemd is" voor dit ene geval. **Als een agent
120
+ * expliciet om een antwoord heeft gevraagd, ís het volgende wat een collega schrijft dat antwoord** —
121
+ * dan iemand dwingen zijn eigen agent te @-noemen op de vraag die die agent net zelf stelde is een
122
+ * ritueel, niet een signaal. De oude vrees ("twee collega's die overleggen kosten elk een beurt")
123
+ * blijft gedekt door de rand eromheen: `waitingOn` moet `"reply"` zijn, en zodra hij op een taak
124
+ * wacht of nog bezig is, is de vermelding weer de enige weg naar binnen.
118
125
  *
119
- * Een vermelding is precies de vraag die de engine zelf niet kan beantwoorden: *is dit aan míjn
120
- * agent gericht?* Dezelfde vorm als {@link assignmentMismatch} bij het toewijzen — expliciet, want
121
- * geraden zou stil het verkeerde doen. En het maakt zelf-wekken onmogelijk: een agent noemt
122
- * zichzelf niet.
126
+ * @param waitingOn Waarop de opdracht wacht. Weggelaten = alleen de regels 1-3 (het gedrag van
127
+ * vóór regel 4), zodat een beller die het niet weet nooit te ruim wekt.
123
128
  */
124
- export declare function wakesAssignment(activity: Activity, agentId: string): boolean;
129
+ export declare function wakesAssignment(activity: Activity, agentId: string, waitingOn?: WaitingOn["on"]): boolean;
125
130
  /** Het onderwerp van een opdracht als één verwijzing, of `undefined`. */
126
131
  export declare function assignmentSubject(assignment: {
127
132
  subjectKind?: string;
@@ -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
@@ -0,0 +1,22 @@
1
+ import { TimeEntry, TimeRounding } from './types';
2
+ /**
3
+ * De verstreken tijd van een regel, in seconden.
4
+ *
5
+ * In `domain` en niet in de app-server, omdat client én server hetzelfde antwoord moeten geven:
6
+ * de server stempelt de duur bij het stoppen, het paneel laat 'm ondertussen elke seconde
7
+ * oplopen. Zou het paneel zelf tellen vanaf een eigen nulpunt, dan loopt de zichtbare klok weg
8
+ * van wat er uiteindelijk geboekt wordt zodra een tabblad even slaapt.
9
+ */
10
+ export declare function elapsedSeconds(entry: Pick<TimeEntry, "accumulatedSeconds" | "runningSince">, now: number): number;
11
+ /**
12
+ * De geboekte duur die bij een gemeten duur hoort.
13
+ *
14
+ * Naar boven en met een minimum van één eenheid: wie "per 15 minuten" kiest, wil dat een klus
15
+ * van drie minuten een kwartier wordt en niet nul. Bij `exact` is de bodem één minuut, om
16
+ * dezelfde reden — een regel van nul seconden is geen regel.
17
+ */
18
+ export declare function applyRounding(rawSeconds: number, rounding: TimeRounding | undefined): number;
19
+ /** `0:45` / `2:05` — uren en minuten, zoals de dagtotalen in het paneel. */
20
+ export declare function formatHm(totalSeconds: number): string;
21
+ /** `12:34` onder het uur, `1:02:03` erboven — de lopende klok. */
22
+ export declare function formatClock(totalSeconds: number): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './duration';
2
+ export * from './types';
3
+ export * from './work-type';
@@ -0,0 +1,176 @@
1
+ import { OwnerScope } from '../contact/types';
2
+ /**
3
+ * Waar een urenregel in zijn leven staat.
4
+ *
5
+ * `review` is met opzet een **server**-staat en niet iets wat alleen in het paneel leeft: het
6
+ * ontwerp laat je na het stoppen nog afronden, een werksoort kiezen en een notitie bijwerken
7
+ * voordat de regel telt. Zat die tussenstap alleen in de client, dan was een dichtgeslagen
8
+ * tabblad precies het moment waarop het gewerkte uur verdampt. Nu vindt het paneel de regel bij
9
+ * het volgende bezoek gewoon terug en staat de review-kaart er weer.
10
+ *
11
+ * `logged` is de enige staat die meetelt in totalen en in de rapportage.
12
+ */
13
+ export type TimeEntryStatus = "running" | "paused" | "review" | "logged";
14
+ /** Hoe de gemeten tijd naar een boekbare duur wordt vertaald. */
15
+ export type TimeRounding = "exact" | "15" | "30";
16
+ /** Handmatig ingevoerd of daadwerkelijk geklokt — zichtbaar in de rapportage. */
17
+ export type TimeEntrySource = "timer" | "manual";
18
+ export interface TimeEntry {
19
+ id: string;
20
+ organizationId: string;
21
+ /** Wie de tijd schreef. Een regel hoort altijd bij precies één mens. */
22
+ userId: string;
23
+ /**
24
+ * Waarop geboekt wordt: `"interaction:abc"`, `"task:42"`, `"company:xyz"`. Afwezig =
25
+ * algemeen werk zonder object.
26
+ *
27
+ * Bewust een opake sleutel en geen `interactionId`: de app die de soort bezit beantwoordt
28
+ * de toegangsvraag (`assertScopeAccess`), dus urenregistratie hoeft geen enkele andere app
29
+ * te kennen om er tijd op te kunnen schrijven.
30
+ */
31
+ scopeKey?: string;
32
+ /** Menselijk etiket op schrijfmoment ("Gesprek · RE: EYLO"), zodat een lijst leesbaar blijft
33
+ * zonder per regel de bron-app te bevragen. */
34
+ scopeLabel?: string;
35
+ /** Dossiersleutels van de resource, platgeslagen bij schrijven — zie de `keys`-conventie. */
36
+ keys?: string[];
37
+ /** Eigenaarschap zoals de bron-app het ziet; bepaalt wie de regel in een teamweergave ziet. */
38
+ ownerScope?: OwnerScope;
39
+ status: TimeEntryStatus;
40
+ /** Epoch ms, gezet door de server. De client rekent alleen af hoe laat het nu is. */
41
+ startedAt: number;
42
+ /** Epoch ms; gezet zodra de timer stopt. Afwezig zolang hij loopt of gepauzeerd staat. */
43
+ endedAt?: number;
44
+ /**
45
+ * Seconden die vóór het huidige segment al gebankt zijn.
46
+ *
47
+ * Pauzeren mag geen tweede rij opleveren — het ontwerp belooft één regel per klus, met een
48
+ * pauzeknop erin. Daarom telt de server bij elke pauze het gelopen segment hierbij op en
49
+ * laat `runningSince` los; hervatten zet alleen `runningSince` opnieuw. De verstreken tijd
50
+ * is dus altijd `accumulatedSeconds + (runningSince ? now - runningSince : 0)`, en dat
51
+ * antwoord is hetzelfde op elk apparaat.
52
+ */
53
+ accumulatedSeconds: number;
54
+ /** Epoch ms waarop het lopende segment begon. Afwezig = gepauzeerd of gestopt. */
55
+ runningSince?: number;
56
+ /** Wat de klok werkelijk mat. Blijft staan als afronding de geboekte duur optrekt. */
57
+ rawSeconds?: number;
58
+ /**
59
+ * De geboekte duur in seconden — dít telt in totalen en facturatie.
60
+ *
61
+ * Los van `rawSeconds` omdat afronding een keuze van de gebruiker is en geen meting: wie
62
+ * later vraagt "waar komt dat kwartier vandaan" moet de gemeten twee minuten nog kunnen zien.
63
+ */
64
+ durationSeconds?: number;
65
+ rounding?: TimeRounding;
66
+ billable: boolean;
67
+ /** Verwijst naar `WorkType.key`. */
68
+ workType?: string;
69
+ note?: string;
70
+ source: TimeEntrySource;
71
+ createdAt?: number;
72
+ updatedAt?: number;
73
+ }
74
+ /**
75
+ * Een beheerbare werksoort in plaats van een rijtje in code: welke soorten een organisatie
76
+ * kent verschilt per bedrijf, en de soort bepaalt of iets standaard declarabel is.
77
+ */
78
+ export interface WorkType {
79
+ id: string;
80
+ organizationId: string;
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
+ */
89
+ key: string;
90
+ label: string;
91
+ /** Standaardwaarde voor `TimeEntry.billable`; per regel te overrulen. */
92
+ defaultBillable: boolean;
93
+ /** Meegenomen als dimensie in de rapportage. */
94
+ reportable?: boolean;
95
+ order?: number;
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
+ }>;
110
+ createdAt?: number;
111
+ updatedAt?: number;
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
+ }
128
+ /** Een kandidaat waarop de gebruiker kan klokken, aangeboden in de leegstaat van het paneel. */
129
+ export interface TimeTarget {
130
+ scopeKey?: string;
131
+ /** Soortlabel boven de titel ("GESPREK OP JE SCHERM", "TAAK"). */
132
+ kindLabel: string;
133
+ title: string;
134
+ /** Lucide-icoonnaam. */
135
+ icon: string;
136
+ keys?: string[];
137
+ }
138
+ export interface StartTimerRequest {
139
+ scopeKey?: string;
140
+ scopeLabel?: string;
141
+ keys?: string[];
142
+ workType?: string;
143
+ note?: string;
144
+ }
145
+ /** Wat er bij het opslaan van een regel nog aan te passen valt. */
146
+ export interface SaveTimeEntryRequest {
147
+ id: string;
148
+ rounding?: TimeRounding;
149
+ workType?: string;
150
+ billable?: boolean;
151
+ note?: string;
152
+ /** Naar een ander object boeken dan waar de timer op startte. */
153
+ scopeKey?: string;
154
+ scopeLabel?: string;
155
+ keys?: string[];
156
+ }
157
+ export interface ManualTimeEntryRequest {
158
+ scopeKey?: string;
159
+ scopeLabel?: string;
160
+ keys?: string[];
161
+ /** Epoch ms; de dag waarop de regel valt. */
162
+ startedAt: number;
163
+ durationSeconds: number;
164
+ workType?: string;
165
+ billable?: boolean;
166
+ note?: string;
167
+ }
168
+ /** Wat het paneel in één keer ophaalt: de lopende timer plus de dag eromheen. */
169
+ export interface TimeDaySummary {
170
+ /** De regel die nu loopt, gepauzeerd staat of op afronding wacht — er is er hooguit één. */
171
+ active?: TimeEntry;
172
+ /** Afgeronde regels van de opgevraagde dag, oplopend op starttijd. */
173
+ entries: TimeEntry[];
174
+ totalSeconds: number;
175
+ billableSeconds: number;
176
+ }
@@ -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 {};