@opencxh/domain 1.161.0 → 1.164.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.
- package/dist/entities/custom-field-def/types.d.ts +29 -2
- package/dist/entities/time-entry/index.d.ts +1 -0
- package/dist/entities/time-entry/types.d.ts +36 -1
- package/dist/entities/time-entry/work-type.d.ts +44 -0
- package/dist/entities/time-entry/work-type.test.d.ts +1 -0
- package/dist/entities/work/index.d.ts +3 -0
- package/dist/entities/work/keys.d.ts +57 -0
- package/dist/entities/work/keys.test.d.ts +1 -0
- package/dist/entities/work/ladder.d.ts +97 -0
- package/dist/entities/work/ladder.test.d.ts +1 -0
- package/dist/entities/work/types.d.ts +255 -0
- package/dist/index.cjs +8 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +930 -790
- package/dist/platform/resource-source.d.ts +94 -0
- package/dist/platform/scope.d.ts +19 -0
- package/dist/platform/sdk.d.ts +14 -0
- package/package.json +1 -1
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
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
|
|
@@ -78,7 +78,14 @@ export interface TimeEntry {
|
|
|
78
78
|
export interface WorkType {
|
|
79
79
|
id: string;
|
|
80
80
|
organizationId: string;
|
|
81
|
-
/**
|
|
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,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
|
+
}
|