@7365admin1/layer-common 3.2.1 → 3.2.2-staging.100

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.
Files changed (123) hide show
  1. package/.changeset/camera-wall-gated-endpoint.md +47 -0
  2. package/.changeset/camera-wall-plain-english.md +60 -0
  3. package/.changeset/camera-wall-selection-and-guidance.md +65 -0
  4. package/.changeset/dark-primary-contrast.md +39 -0
  5. package/.changeset/dashboard-dark-theme-contrast.md +15 -0
  6. package/.changeset/notification-preferences.md +17 -0
  7. package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
  8. package/.changeset/service-provider-invitation-actions.md +28 -0
  9. package/assets/css/primitives.css +680 -0
  10. package/assets/css/screens.css +541 -0
  11. package/assets/css/shell.css +258 -0
  12. package/assets/css/tokens.css +220 -0
  13. package/components/AccessCardQrTagging.vue +1 -1
  14. package/components/AccessManagement.vue +53 -72
  15. package/components/AppButton.vue +52 -0
  16. package/components/AppCard.vue +20 -0
  17. package/components/AppDateField.vue +35 -0
  18. package/components/AppField.vue +46 -0
  19. package/components/AppKpiTile.vue +65 -0
  20. package/components/AppSegmented.vue +38 -0
  21. package/components/AppSelect.vue +203 -0
  22. package/components/AreaChecklistHistoryMain.vue +6 -2
  23. package/components/AreaMain.vue +9 -17
  24. package/components/AttendanceMain.vue +4 -3
  25. package/components/Avatar/Main.vue +54 -2
  26. package/components/BillingMain.vue +26 -30
  27. package/components/BuildingManagement/buildings.vue +27 -38
  28. package/components/BuildingManagement/units.vue +26 -42
  29. package/components/BulletinBoardManagement.vue +23 -10
  30. package/components/CameraWall.vue +1034 -0
  31. package/components/CameraWallTile.vue +818 -0
  32. package/components/CardToolbar.vue +37 -0
  33. package/components/CleaningScheduleMain.vue +29 -46
  34. package/components/ClientMain.vue +90 -78
  35. package/components/DashboardEmptyState.vue +11 -1
  36. package/components/DashboardMain.vue +349 -275
  37. package/components/DashboardPanel.vue +1 -1
  38. package/components/DocumentManagement.vue +129 -93
  39. package/components/EntryPassInformation.vue +4 -4
  40. package/components/EntryPassMain.vue +49 -27
  41. package/components/EquipmentItemMain.vue +4 -6
  42. package/components/EquipmentManagementMain.vue +9 -7
  43. package/components/FeedbackMain.vue +42 -56
  44. package/components/FormDialog.vue +60 -21
  45. package/components/HidAccessLogDashboard.vue +119 -44
  46. package/components/HidCameraCaptureDialog.vue +199 -0
  47. package/components/HidIntercomManagement.vue +825 -202
  48. package/components/HidQrCodeConfiguration.vue +927 -93
  49. package/components/HidServiceSettingsPanel.vue +20 -1
  50. package/components/HidUserEnrollment.vue +454 -106
  51. package/components/InvitationMain.vue +46 -61
  52. package/components/Layout/Header.vue +289 -37
  53. package/components/Layout/NavigationDrawer.vue +340 -30
  54. package/components/ManageChecklistMain.vue +17 -17
  55. package/components/MemberMain.vue +79 -60
  56. package/components/MyAttendanceMain.vue +3 -5
  57. package/components/NavigationItem.vue +9 -0
  58. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  59. package/components/NotificationSettings.vue +361 -0
  60. package/components/OnlineFormConfigurationForm.vue +620 -224
  61. package/components/OnlineFormFill.vue +36 -8
  62. package/components/OnlineFormsConfiguration.vue +7 -3
  63. package/components/PageHeader.vue +43 -0
  64. package/components/PassInformation.vue +104 -45
  65. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  66. package/components/QrTemplate/PrintDialog.vue +209 -196
  67. package/components/RolePermissionMain.vue +184 -91
  68. package/components/ScanVisitorQRCode.vue +35 -7
  69. package/components/ScheduleAreaMain.vue +15 -49
  70. package/components/ScheduleTaskMain.vue +23 -26
  71. package/components/ScheduleTastTicketMain.vue +18 -43
  72. package/components/ServiceProviderInvitationPrompt.vue +150 -0
  73. package/components/ServiceProviderMain.vue +346 -157
  74. package/components/SiteSettings.vue +25 -10
  75. package/components/SpecificAttr.vue +10 -2
  76. package/components/StatusChip.vue +93 -0
  77. package/components/SwitchContext.vue +33 -1
  78. package/components/TableHygiene.vue +29 -46
  79. package/components/TableList.vue +1 -1
  80. package/components/TableListSecondary.vue +1 -1
  81. package/components/TableMain.vue +161 -77
  82. package/components/TableV3.vue +61 -66
  83. package/components/UnitMain.vue +9 -17
  84. package/components/VehicleManagement.vue +20 -9
  85. package/components/VisitorForm.vue +294 -78
  86. package/components/VisitorManagement.vue +221 -71
  87. package/components/VisitorSocketPopUp.vue +56 -2
  88. package/components/WorkOrder/Main.vue +42 -51
  89. package/composables/useComment.ts +3 -3
  90. package/composables/useHidAmico.ts +151 -0
  91. package/composables/useHidNavigation.ts +0 -7
  92. package/composables/useLocalAuth.ts +9 -36
  93. package/composables/useNotificationPreference.ts +87 -0
  94. package/composables/useOnlineFormPages.ts +2 -0
  95. package/composables/useOrg.ts +1 -1
  96. package/composables/useRole.ts +22 -0
  97. package/composables/useServiceProviderInvitation.ts +145 -0
  98. package/composables/useSipWebPhone.ts +336 -0
  99. package/composables/useSiteSettings.ts +50 -0
  100. package/composables/useThemePreference.ts +63 -0
  101. package/composables/useVisitor.ts +6 -1
  102. package/composables/useVisitorSocket.ts +26 -0
  103. package/composables/useWebUsb.ts +127 -37
  104. package/nuxt.config.ts +31 -0
  105. package/package.json +4 -1
  106. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  107. package/pages/notification-settings.vue +19 -0
  108. package/plugins/secure-member.client.ts +21 -56
  109. package/plugins/vuetify.ts +68 -9
  110. package/test/visitor-socket.test.mjs +36 -0
  111. package/types/org.d.ts +2 -0
  112. package/types/site.d.ts +71 -0
  113. package/utils/camera-wall.test.ts +571 -0
  114. package/utils/camera-wall.ts +926 -0
  115. package/utils/nav-sections.test.ts +190 -0
  116. package/utils/nav-sections.ts +151 -0
  117. package/utils/status.test.ts +77 -0
  118. package/utils/status.ts +90 -0
  119. package/utils/theme.test.ts +534 -0
  120. package/utils/theme.ts +294 -0
  121. package/components/HidIdentityMapping.vue +0 -975
  122. package/middleware/member.ts +0 -4
  123. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -0,0 +1,190 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { NAV_SECTIONS, withSections } from "./nav-sections.ts";
5
+
6
+ /** The Security menu, item for item and in `web-app-security`'s own order. */
7
+ const SECURITY = [
8
+ "Dashboard",
9
+ "Feedbacks",
10
+ "Work Orders",
11
+ "Vehicle Mgmt",
12
+ "Building Mgmt",
13
+ "Visitor Mgmt",
14
+ "Pass & Key Mgmt",
15
+ "HID Face Reader",
16
+ "Daily Occurrence Books",
17
+ "Manpower",
18
+ "Virtual Patrol",
19
+ "Robot Mgmt",
20
+ "Incident Reports",
21
+ "Bulletin Board",
22
+ "Attendance",
23
+ "My Attendance",
24
+ "Invitations",
25
+ "Members",
26
+ "Roles & Permissions",
27
+ "Settings",
28
+ ].map((title) => ({ title }));
29
+
30
+ /**
31
+ * THE RULE THAT MATTERS. Grouping may reorder the rail; it may never change
32
+ * the menu. Same objects, same count - nothing added, hidden or duplicated.
33
+ */
34
+ test("every menu item comes back exactly once, and nothing else does", () => {
35
+ const out = withSections(SECURITY);
36
+
37
+ assert.equal(out.length, SECURITY.length);
38
+ assert.deepEqual(
39
+ new Set(out.map((e) => e.item)),
40
+ new Set(SECURITY)
41
+ );
42
+ });
43
+
44
+ test("Security is labelled the way the design draws it", () => {
45
+ const labelled = withSections(SECURITY)
46
+ .filter((e) => e.section)
47
+ .map((e) => `${e.section}: ${e.item.title}`);
48
+
49
+ assert.deepEqual(labelled, [
50
+ "OVERVIEW: Dashboard",
51
+ "SERVICE DESK: Feedbacks",
52
+ "PROPERTY: Vehicle Mgmt",
53
+ "SECURITY OPERATIONS: HID Face Reader",
54
+ "COMMUNITY: Bulletin Board",
55
+ "WORKFORCE: Attendance",
56
+ "ADMINISTRATION: Invitations",
57
+ ]);
58
+ });
59
+
60
+ /**
61
+ * A permission-gated menu is a SUBSET, and every application's is different.
62
+ * Dropping the items a user cannot see must not leave a heading behind or move
63
+ * one onto the wrong item.
64
+ */
65
+ test("a user who can only see three modules gets three correct headings", () => {
66
+ const out = withSections(
67
+ [{ title: "Dashboard" }, { title: "Work Orders" }, { title: "Settings" }]
68
+ );
69
+
70
+ assert.deepEqual(
71
+ out.map((e) => e.section),
72
+ ["OVERVIEW", "SERVICE DESK", "ADMINISTRATION"]
73
+ );
74
+ });
75
+
76
+ /**
77
+ * The eleven applications add menu items without touching this layer. An
78
+ * unknown title must appear where its author put it, under the heading it
79
+ * follows - never suppressed, and never given a heading of its own.
80
+ */
81
+ test("an unmapped module inherits the section above it", () => {
82
+ const out = withSections([
83
+ { title: "Dashboard" },
84
+ { title: "Something New" },
85
+ { title: "Members" },
86
+ ]);
87
+
88
+ assert.deepEqual(
89
+ out.map((e) => e.section),
90
+ ["OVERVIEW", "", "ADMINISTRATION"]
91
+ );
92
+ assert.equal(out[1].item.title, "Something New");
93
+ });
94
+
95
+ test("an unmapped FIRST item prints no heading rather than a blank one", () => {
96
+ const out = withSections([{ title: "Something New" }, { title: "Members" }]);
97
+
98
+ assert.deepEqual(
99
+ out.map((e) => e.section),
100
+ ["", "ADMINISTRATION"]
101
+ );
102
+ });
103
+
104
+ /**
105
+ * The applications do not agree on order: Landscape lists Feedbacks after its
106
+ * equipment modules. Those two Feedbacks/Work Orders items are collected under
107
+ * ONE SERVICE DESK heading instead of printing the heading twice.
108
+ */
109
+ test("a section that comes back later is grouped, not labelled twice", () => {
110
+ const out = withSections([
111
+ { title: "Feedbacks" },
112
+ { title: "Equipment Items" },
113
+ { title: "Work Orders" },
114
+ ]);
115
+
116
+ assert.deepEqual(
117
+ out.map((e) => `${e.section}|${e.item.title}`),
118
+ [
119
+ "SERVICE DESK|Feedbacks",
120
+ "|Work Orders",
121
+ "EQUIPMENT|Equipment Items",
122
+ ]
123
+ );
124
+ });
125
+
126
+ /**
127
+ * Property Management is the screen that showed the defect: its own menu order
128
+ * printed SERVICE DESK, PROPERTY and SECURITY OPERATIONS twice each.
129
+ */
130
+ test("Property Management prints each heading exactly once", () => {
131
+ const PM = [
132
+ "Dashboard",
133
+ "Service Providers",
134
+ "Feedbacks",
135
+ "Work Orders",
136
+ "Facility Mgmt",
137
+ "Pass & Key Mgmt",
138
+ "HID Face Reader",
139
+ "Online Forms",
140
+ "NFC Patrol",
141
+ "Bulletin Board",
142
+ "Bulletin Videos",
143
+ "Visitor Mgmt",
144
+ "Document Mgmt",
145
+ "Access Mgmt",
146
+ ].map((title) => ({ title }));
147
+
148
+ const out = withSections(PM);
149
+ const headings = out.map((e) => e.section).filter(Boolean);
150
+
151
+ assert.deepEqual(headings, [
152
+ "OVERVIEW",
153
+ "SERVICE DESK",
154
+ "PROPERTY",
155
+ "SECURITY OPERATIONS",
156
+ "COMMUNITY",
157
+ ]);
158
+ assert.equal(new Set(headings).size, headings.length);
159
+ assert.deepEqual(
160
+ out.map((e) => e.item.title),
161
+ [
162
+ "Dashboard",
163
+ "Service Providers",
164
+ "Feedbacks",
165
+ "Work Orders",
166
+ "Online Forms",
167
+ "Facility Mgmt",
168
+ "Pass & Key Mgmt",
169
+ "Visitor Mgmt",
170
+ "Document Mgmt",
171
+ "Access Mgmt",
172
+ "HID Face Reader",
173
+ "NFC Patrol",
174
+ "Bulletin Board",
175
+ "Bulletin Videos",
176
+ ]
177
+ );
178
+ });
179
+
180
+ test("a missing or empty title does not throw", () => {
181
+ const out = withSections([{}, { title: "" }] as Array<{ title?: string }>);
182
+ assert.equal(out.length, 2);
183
+ });
184
+
185
+ /** Every section name is the uppercase form the type scale is drawn for. */
186
+ test("section names are uppercase", () => {
187
+ for (const section of Object.values(NAV_SECTIONS)) {
188
+ assert.equal(section, section.toUpperCase(), section);
189
+ }
190
+ });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * THE GROUPED SIDEBAR - one heading per section, each printed once.
3
+ *
4
+ * The design draws the sidebar in labelled sections (OVERVIEW, SERVICE DESK,
5
+ * PROPERTY, SECURITY OPERATIONS, COMMUNITY, WORKFORCE, ADMINISTRATION). Each
6
+ * application builds its own menu array in its `layouts/default.vue`, gated
7
+ * item by item on permissions.
8
+ *
9
+ * The applications do not agree on the order they list their modules in
10
+ * (Property Management interleaves five sections; Landscape puts Feedbacks
11
+ * after Equipment). Walking that order and printing a heading whenever the
12
+ * section changed - what this file used to do - made Property Management draw
13
+ * SERVICE DESK, PROPERTY and SECURITY OPERATIONS TWICE each. It reads as a
14
+ * bug, so the items are now GROUPED:
15
+ *
16
+ * - every item is collected under its section, and each heading is printed
17
+ * exactly once, in the order the design lists the sections (which is the
18
+ * order they are declared in the map below).
19
+ * - inside a section the application's own order is preserved, so items that
20
+ * belong together stay in the sequence their author chose.
21
+ * - a title this map has never heard of inherits the section of the item
22
+ * above it, so a new menu item added by any of the eleven applications is
23
+ * grouped with its neighbours rather than disappearing or forcing a stray
24
+ * heading.
25
+ * - an item with no section at all (an unmapped FIRST item) leads the rail
26
+ * with no heading, exactly as before.
27
+ *
28
+ * This changes what ORDER the items are drawn in and nothing else. No item is
29
+ * added, hidden, renamed, re-routed or re-gated - the array that comes out
30
+ * holds the same objects as the array that went in.
31
+ */
32
+
33
+ export const NAV_SECTIONS: Record<string, string> = {
34
+ // OVERVIEW
35
+ Dashboard: "OVERVIEW",
36
+ Home: "OVERVIEW",
37
+
38
+ // SERVICE DESK
39
+ "Service Providers": "SERVICE DESK",
40
+ Feedbacks: "SERVICE DESK",
41
+ "Work Orders": "SERVICE DESK",
42
+ "Online Forms": "SERVICE DESK",
43
+
44
+ // SERVICE DELIVERY - the five service applications' own working modules
45
+ "Cleaning Schedule": "SERVICE DELIVERY",
46
+ "Landscape Schedule": "SERVICE DELIVERY",
47
+ "Pest Schedule": "SERVICE DELIVERY",
48
+ "Pool Schedule": "SERVICE DELIVERY",
49
+ "Technician Schedule": "SERVICE DELIVERY",
50
+ Area: "SERVICE DELIVERY",
51
+ Unit: "SERVICE DELIVERY",
52
+ "Schedule Task": "SERVICE DELIVERY",
53
+
54
+ // EQUIPMENT
55
+ "Equipment Management": "EQUIPMENT",
56
+ "Equipment Items": "EQUIPMENT",
57
+
58
+ // PROPERTY
59
+ "Vehicle Mgmt": "PROPERTY",
60
+ "Building Mgmt": "PROPERTY",
61
+ "Visitor Mgmt": "PROPERTY",
62
+ "Pass & Key Mgmt": "PROPERTY",
63
+ "Facility Mgmt": "PROPERTY",
64
+ "People Mgmt": "PROPERTY",
65
+ "Document Mgmt": "PROPERTY",
66
+ "Access Mgmt": "PROPERTY",
67
+ "Event Mgmt": "PROPERTY",
68
+ "Emergency Contacts": "PROPERTY",
69
+ "Online Billing": "PROPERTY",
70
+ "SOA Mgmt": "PROPERTY",
71
+
72
+ // SECURITY OPERATIONS
73
+ "HID Face Reader": "SECURITY OPERATIONS",
74
+ "Daily Occurrence Books": "SECURITY OPERATIONS",
75
+ Manpower: "SECURITY OPERATIONS",
76
+ "Virtual Patrol": "SECURITY OPERATIONS",
77
+ "NFC Patrol": "SECURITY OPERATIONS",
78
+ "Robot Mgmt": "SECURITY OPERATIONS",
79
+ "Incident Reports": "SECURITY OPERATIONS",
80
+ CCTV: "SECURITY OPERATIONS",
81
+
82
+ // COMMUNITY
83
+ "Bulletin Board": "COMMUNITY",
84
+ "Bulletin Videos": "COMMUNITY",
85
+
86
+ // WORKFORCE
87
+ Attendance: "WORKFORCE",
88
+ "My Attendance": "WORKFORCE",
89
+
90
+ // ADMINISTRATION
91
+ Invitations: "ADMINISTRATION",
92
+ "SP Approvals": "ADMINISTRATION",
93
+ Members: "ADMINISTRATION",
94
+ "Roles & Permissions": "ADMINISTRATION",
95
+ Settings: "ADMINISTRATION",
96
+ "Site Settings": "ADMINISTRATION",
97
+ Organizations: "ADMINISTRATION",
98
+ Users: "ADMINISTRATION",
99
+ "Promo Codes": "ADMINISTRATION",
100
+
101
+ // ACCOUNT - `web-app-account` is a personal-settings application, not a site
102
+ "Personal Info": "ACCOUNT",
103
+ "Payment Methods": "ACCOUNT",
104
+
105
+ // SERVICES - the account application's list of the products a user can enter
106
+ "Property Management": "SERVICES",
107
+ Security: "SERVICES",
108
+ Hygiene: "SERVICES",
109
+ "Mechanical & Electrical": "SERVICES",
110
+ "Pest Control Services": "SERVICES",
111
+ "Landscaping Services": "SERVICES",
112
+ "Pool Maintenance Services": "SERVICES",
113
+ };
114
+
115
+ export type TSectionedNavItem<T> = { item: T; section: string };
116
+
117
+ /** The design's section order - the order they are declared above. */
118
+ export const SECTION_ORDER: string[] = [
119
+ ...new Set(Object.values(NAV_SECTIONS)),
120
+ ];
121
+
122
+ /**
123
+ * Groups the menu by section and tags each item with the heading to print
124
+ * above it, or "" when the heading is already printed. Same length, same
125
+ * objects; only the order changes.
126
+ */
127
+ export function withSections<T extends { title?: string }>(
128
+ items: T[]
129
+ ): Array<TSectionedNavItem<T>> {
130
+ // An unmapped title inherits the section of the item above it.
131
+ let current = "";
132
+ const resolved = items.map((item, index) => {
133
+ current = NAV_SECTIONS[String(item?.title ?? "")] ?? current;
134
+ return { item, section: current, index };
135
+ });
136
+
137
+ // Sectionless items (an unmapped first item) rank -1 and lead the rail.
138
+ // Inside a section the application's own order is kept.
139
+ const grouped = [...resolved].sort(
140
+ (a, b) =>
141
+ SECTION_ORDER.indexOf(a.section) - SECTION_ORDER.indexOf(b.section) ||
142
+ a.index - b.index
143
+ );
144
+
145
+ let printed = "";
146
+ return grouped.map(({ item, section }) => {
147
+ const label = section === printed ? "" : section;
148
+ printed = section;
149
+ return { item, section: label };
150
+ });
151
+ }
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { statusTone, statusToneClass } from "./status.ts";
5
+
6
+ test("the handoff's mapping, word for word", () => {
7
+ for (const s of ["Completed", "Paid", "Available", "Open"]) {
8
+ assert.equal(statusTone(s), "ok", s);
9
+ }
10
+
11
+ assert.equal(statusTone("Pending"), "warn");
12
+
13
+ for (const s of ["In Use", "Closed", "Checkout"]) {
14
+ assert.equal(statusTone(s), "err", s);
15
+ }
16
+
17
+ assert.equal(statusTone("Role"), "info");
18
+ });
19
+
20
+ /** The same status arrives spelled three ways from three endpoints. */
21
+ test("casing, spacing and underscores do not change a status's colour", () => {
22
+ for (const s of ["in use", "IN USE", "In_Use", "in-use", " In Use "]) {
23
+ assert.equal(statusTone(s), "err", s);
24
+ }
25
+ });
26
+
27
+ /**
28
+ * The important one. A status the design never named must NOT be guessed into
29
+ * a colour - a wrong green on a failed job reads as success. Neutral is
30
+ * visible, honest, and gets it noticed.
31
+ */
32
+ test("an unmapped status comes out neutral rather than guessed", () => {
33
+ for (const s of ["Cancelled", "Overdue", "Draft", "", null, undefined]) {
34
+ assert.equal(statusTone(s), "neutral", String(s));
35
+ }
36
+ });
37
+
38
+ /**
39
+ * PHASE 4. Each of these words is on a module screen today with a colour a
40
+ * per-screen `getStatusColor` gave it. The list has to keep giving it the SAME
41
+ * tone, or adopting the shared chip would quietly repaint a status.
42
+ */
43
+ test("the product's own status words keep the tone the screens already gave them", () => {
44
+ for (const s of ["Accepted", "Approved", "Resolved", "Active"]) {
45
+ assert.equal(statusTone(s), "ok", s);
46
+ }
47
+
48
+ for (const s of ["Rejected", "Deleted", "Suspended"]) {
49
+ assert.equal(statusTone(s), "err", s);
50
+ }
51
+
52
+ for (const s of ["In Progress", "In-Progress", "To-Do", "Awaiting Approval"]) {
53
+ assert.equal(statusTone(s), "warn", s);
54
+ }
55
+
56
+ for (const s of ["Ongoing", "For Review"]) {
57
+ assert.equal(statusTone(s), "info", s);
58
+ }
59
+
60
+ for (const s of ["Inactive", "Expired"]) {
61
+ assert.equal(statusTone(s), "neutral", s);
62
+ }
63
+ });
64
+
65
+ /**
66
+ * The one place the handoff and the old per-screen colour disagree: `Open` was
67
+ * grey on the cleaning schedules and the design names it `ok`. Pinned so the
68
+ * disagreement is a decision on the record rather than a surprise.
69
+ */
70
+ test("Open follows the design, not the old grey", () => {
71
+ assert.equal(statusTone("open"), "ok");
72
+ });
73
+
74
+ test("the class name matches what tokens.css actually defines", () => {
75
+ assert.equal(statusToneClass("Paid"), "tone-ok");
76
+ assert.equal(statusToneClass("Whatever"), "tone-neutral");
77
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * THE SEMANTIC STATUS MAPPING, IN ONE PLACE.
3
+ *
4
+ * The design assigns a colour to a status WORD rather than to a module, so the
5
+ * same word has to look the same wherever it appears - "Open" on a Daily
6
+ * Occurrence Book and "Open" on a work order are the same green. Keeping the
7
+ * list here rather than in each screen is what makes that true, and is what
8
+ * Phases 3 and 4 read when they restyle the chips.
9
+ *
10
+ * Straight from the handoff:
11
+ * Completed / Paid / Available / Open -> ok
12
+ * Pending -> warn
13
+ * In Use / Closed / Checkout -> err
14
+ * Role and info tags -> info
15
+ *
16
+ * Anything not listed is `neutral` on purpose. A status this product has and
17
+ * the design did not name must NOT be guessed into a colour - a wrong green on
18
+ * a failed job is worse than a grey one - so it comes out neutral and visible,
19
+ * which is how it gets noticed and added.
20
+ */
21
+ export type TStatusTone = "ok" | "warn" | "err" | "info" | "neutral";
22
+
23
+ const TONES: Record<string, TStatusTone> = {
24
+ completed: "ok",
25
+ paid: "ok",
26
+ available: "ok",
27
+ open: "ok",
28
+
29
+ pending: "warn",
30
+
31
+ "in use": "err",
32
+ closed: "err",
33
+ checkout: "err",
34
+
35
+ role: "info",
36
+ info: "info",
37
+
38
+ /**
39
+ * PHASE 4 - THE WORDS THIS PRODUCT ACTUALLY USES.
40
+ *
41
+ * The handoff names ten words; the module screens show more than that, and
42
+ * each of them is ALREADY painted a colour today by a per-screen
43
+ * `getStatusColor`. So these are not new opinions - each one is the tone the
44
+ * screens already give that word, moved into the one list so that "Ongoing"
45
+ * is the same blue on a cleaning schedule and on a scheduled area. Where the
46
+ * old per-screen colour and the design disagree the design wins, and there
47
+ * is exactly one of those: `Open` was grey and the handoff names it `ok`.
48
+ *
49
+ * Anything still unlisted stays neutral, deliberately - see above.
50
+ */
51
+ ongoing: "info", // was `primary`
52
+ "in progress": "warn", // was #FB8C00, orange
53
+ "for review": "info", // was `primary`
54
+ "to do": "warn", // was `orange`
55
+
56
+ accepted: "ok", // was `success`
57
+ approved: "ok",
58
+ resolved: "ok", // was #4CAF50, green
59
+ active: "ok", // was `success`
60
+
61
+ rejected: "err", // was `error`
62
+ deleted: "err", // was `error`
63
+ suspended: "err", // was `error`
64
+
65
+ complete: "ok",
66
+
67
+ inactive: "neutral", // was `grey`
68
+ "in active": "neutral", // the same word, printed "In-Active"
69
+ declined: "neutral", // was Vuetify's `default`
70
+ expired: "neutral", // was `grey`
71
+
72
+ "awaiting approval": "warn", // a waiting state, like Pending
73
+ };
74
+
75
+ /**
76
+ * The tone for a status word. Case- and space-insensitive, because the same
77
+ * status arrives as "In Use", "in_use" and "IN USE" from different endpoints.
78
+ */
79
+ export function statusTone(status: string | null | undefined): TStatusTone {
80
+ if (!status) return "neutral";
81
+
82
+ const key = String(status).trim().toLowerCase().replace(/[_-]+/g, " ");
83
+
84
+ return TONES[key] ?? "neutral";
85
+ }
86
+
87
+ /** The class `assets/css/tokens.css` paints that tone with. */
88
+ export function statusToneClass(status: string | null | undefined): string {
89
+ return `tone-${statusTone(status)}`;
90
+ }