@7365admin1/layer-common 3.2.2-staging.190 → 3.2.2-staging.192
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/components/DashboardMain.vue +16 -6
- package/package.json +1 -1
- package/utils/data.test.ts +72 -0
- package/utils/data.ts +25 -2
|
@@ -724,7 +724,7 @@
|
|
|
724
724
|
<v-col v-if="isWidgetVisible('todayAttentions')" cols="12" lg="6">
|
|
725
725
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
726
726
|
<div class="figma-panel-header">
|
|
727
|
-
<h2>
|
|
727
|
+
<h2>Needs Attention</h2>
|
|
728
728
|
</div>
|
|
729
729
|
<div class="figma-panel-body">
|
|
730
730
|
<div v-if="todayAttentions.length" class="figma-list">
|
|
@@ -2084,9 +2084,9 @@ const customizeWidgets = computed(() => {
|
|
|
2084
2084
|
},
|
|
2085
2085
|
{
|
|
2086
2086
|
key: "todayAttentions",
|
|
2087
|
-
title: "
|
|
2087
|
+
title: "Needs Attention",
|
|
2088
2088
|
description:
|
|
2089
|
-
"
|
|
2089
|
+
"Everything still pending or unacknowledged, not only today's.",
|
|
2090
2090
|
},
|
|
2091
2091
|
{
|
|
2092
2092
|
key: "workOrderStatus",
|
|
@@ -2487,6 +2487,12 @@ const dashboardTitle = computed(() => {
|
|
|
2487
2487
|
return `${getServiceDisplayName(props.serviceType || appName)} - Dashboard`;
|
|
2488
2488
|
});
|
|
2489
2489
|
|
|
2490
|
+
const taskScheduleTitle = computed(() => {
|
|
2491
|
+
if (selectedRange.value === "This Month") return "This Month's Task Schedule";
|
|
2492
|
+
if (selectedRange.value === "This Week") return "This Week's Task Schedule";
|
|
2493
|
+
return "Today's Task Schedule";
|
|
2494
|
+
});
|
|
2495
|
+
|
|
2490
2496
|
// ─── Primary list / staff config ─────────────────────────────────────────────
|
|
2491
2497
|
|
|
2492
2498
|
const primaryListConfig = computed(() =>
|
|
@@ -2497,7 +2503,11 @@ const primaryListConfig = computed(() =>
|
|
|
2497
2503
|
keys: ["activePatrolItems", "patrolItems"],
|
|
2498
2504
|
}
|
|
2499
2505
|
: {
|
|
2500
|
-
|
|
2506
|
+
// The endpoint behind this panel takes the selected period
|
|
2507
|
+
// (`getServiceTodayTaskSchedule` -> `getDateRange(period)`), so on the
|
|
2508
|
+
// Week or Month range the list was a week's or a month's work under a
|
|
2509
|
+
// heading that said "Today's".
|
|
2510
|
+
title: taskScheduleTitle.value,
|
|
2501
2511
|
route: "scheduleTask",
|
|
2502
2512
|
keys: [
|
|
2503
2513
|
"taskScheduleItems",
|
|
@@ -2780,7 +2790,7 @@ const printTableSections = computed<PrintTableSection[]>(() => {
|
|
|
2780
2790
|
},
|
|
2781
2791
|
{
|
|
2782
2792
|
key: "todayAttentions",
|
|
2783
|
-
title: "
|
|
2793
|
+
title: "Needs Attention",
|
|
2784
2794
|
columns: [
|
|
2785
2795
|
{ key: "title", label: "Attention", width: "40%", strong: true },
|
|
2786
2796
|
{ key: "subtitle", label: "Details / Subtitle", width: "60%" },
|
|
@@ -3727,7 +3737,7 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3727
3737
|
}
|
|
3728
3738
|
wordHtml += `</tbody></table>`;
|
|
3729
3739
|
|
|
3730
|
-
wordHtml += `<div class="section-title">
|
|
3740
|
+
wordHtml += `<div class="section-title">Needs Attention</div>`;
|
|
3731
3741
|
wordHtml += `<table class="data-table">`;
|
|
3732
3742
|
wordHtml += `<thead><tr><th>Attention</th><th>Details / Subtitle</th></tr></thead><tbody>`;
|
|
3733
3743
|
if (todayAttentions.value.length > 0) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.2.2-staging.
|
|
5
|
+
"version": "3.2.2-staging.192",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { findServiceProviderIdBySite } from "./data.ts";
|
|
5
|
+
|
|
6
|
+
const SITE = "68f05fcc0c11062938dadada";
|
|
7
|
+
const OTHER_SITE = "68f05fcc0c11062938d00001";
|
|
8
|
+
const PROVIDER = "68f05fcc0c11062938d00002";
|
|
9
|
+
|
|
10
|
+
test("returns the provider that covers this site", () => {
|
|
11
|
+
assert.equal(
|
|
12
|
+
findServiceProviderIdBySite(
|
|
13
|
+
"service-provider",
|
|
14
|
+
[{ _id: PROVIDER, siteId: SITE }],
|
|
15
|
+
SITE
|
|
16
|
+
),
|
|
17
|
+
PROVIDER
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a non-service-provider account is not scoped by provider at all", () => {
|
|
22
|
+
assert.equal(findServiceProviderIdBySite("customer", [], SITE), null);
|
|
23
|
+
assert.equal(findServiceProviderIdBySite("", undefined, SITE), null);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returning null here would drop the provider filter and show the whole site -
|
|
28
|
+
* another provider's work orders included. Refusing is the correct behaviour.
|
|
29
|
+
*/
|
|
30
|
+
test("refuses rather than falling back to an unscoped request", () => {
|
|
31
|
+
assert.throws(() =>
|
|
32
|
+
findServiceProviderIdBySite(
|
|
33
|
+
"service-provider",
|
|
34
|
+
[{ _id: PROVIDER, siteId: OTHER_SITE }],
|
|
35
|
+
SITE
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The thrown message is rendered verbatim in a toast on six applications, so it
|
|
42
|
+
* is part of the user interface. Assert what it SAYS, not that it throws: the
|
|
43
|
+
* old message passed a "throws" assertion while quoting a raw ObjectId at a
|
|
44
|
+
* security officer and naming no action.
|
|
45
|
+
*/
|
|
46
|
+
test("the message names an action and never quotes an internal id", () => {
|
|
47
|
+
for (const providers of [[{ _id: PROVIDER, siteId: OTHER_SITE }], [], undefined]) {
|
|
48
|
+
let message = "";
|
|
49
|
+
try {
|
|
50
|
+
findServiceProviderIdBySite("service-provider", providers, SITE);
|
|
51
|
+
assert.fail("expected it to refuse");
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
message = error.message;
|
|
54
|
+
}
|
|
55
|
+
assert.doesNotMatch(message, /[0-9a-f]{24}/i, `leaks an id: ${message}`);
|
|
56
|
+
assert.doesNotMatch(message, /siteId/i, `internal wording: ${message}`);
|
|
57
|
+
assert.match(message, /not linked to this site/i);
|
|
58
|
+
assert.match(message, /administrator/i);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* An account with no serviceProviders field at all used to die on
|
|
64
|
+
* `undefined.find(...)` - a TypeError, which the toast renders as a raw
|
|
65
|
+
* JavaScript error instead of the sentence above.
|
|
66
|
+
*/
|
|
67
|
+
test("an account with no provider list still gets the sentence, not a TypeError", () => {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => findServiceProviderIdBySite("service-provider", undefined, SITE),
|
|
70
|
+
(error: any) => error.constructor === Error && !/undefined/.test(error.message)
|
|
71
|
+
);
|
|
72
|
+
});
|
package/utils/data.ts
CHANGED
|
@@ -30,17 +30,40 @@ export const errorConverter = (data: any): string => {
|
|
|
30
30
|
return error;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A service-provider account can only be shown ITS OWN work orders, feedbacks
|
|
35
|
+
* and key logs, so every one of those screens scopes its request by the
|
|
36
|
+
* provider record that links this account to the site currently in the URL.
|
|
37
|
+
*
|
|
38
|
+
* When that link does not exist there is nothing to scope by, and returning
|
|
39
|
+
* null instead would drop the filter and show the WHOLE site - another
|
|
40
|
+
* provider's work orders included. So this still refuses, deliberately.
|
|
41
|
+
*
|
|
42
|
+
* What it refuses WITH is the part that matters. The message is thrown straight
|
|
43
|
+
* into a toast on six applications, so it is written for the person reading it:
|
|
44
|
+
* it says what is missing and what to do about it. It used to read
|
|
45
|
+
* "Service provider with siteId 68f0...dadada not found", which quotes an
|
|
46
|
+
* internal identifier at a security officer and names no action - the screen
|
|
47
|
+
* looked broken when the truth was simply that this site is not one of theirs.
|
|
48
|
+
*
|
|
49
|
+
* The site list in the sidebar offers EVERY site in the organisation, not only
|
|
50
|
+
* the ones this account covers, so an officer can walk into this by picking a
|
|
51
|
+
* neighbouring site. That is why the wording assumes an ordinary mistake and
|
|
52
|
+
* not a fault.
|
|
53
|
+
*/
|
|
33
54
|
export const findServiceProviderIdBySite = (
|
|
34
55
|
type: string,
|
|
35
56
|
serviceProviders: any,
|
|
36
57
|
siteId: string
|
|
37
58
|
) => {
|
|
38
59
|
if (type === "service-provider") {
|
|
39
|
-
const result = serviceProviders
|
|
60
|
+
const result = serviceProviders?.find(
|
|
40
61
|
(item: Record<string, any>) => item.siteId === siteId
|
|
41
62
|
);
|
|
42
63
|
if (!result?._id) {
|
|
43
|
-
throw new Error(
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Your service provider account is not linked to this site, so there is nothing to show here. Choose a site your company covers, or ask your administrator to link your company to this one."
|
|
66
|
+
);
|
|
44
67
|
}
|
|
45
68
|
return result?._id;
|
|
46
69
|
}
|