@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.
- package/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +46 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- 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
|
+
});
|
package/utils/status.ts
ADDED
|
@@ -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
|
+
}
|