@7365admin1/layer-common 3.2.2-staging.138 → 3.2.2-staging.140
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.
|
@@ -958,3 +958,29 @@ select.app-field__input {
|
|
|
958
958
|
min-width: 720px;
|
|
959
959
|
}
|
|
960
960
|
}
|
|
961
|
+
|
|
962
|
+
/* ------------------------------------------------------------------ */
|
|
963
|
+
/* Field label (`components/InputLabel.vue`, 125 caller files). */
|
|
964
|
+
/* */
|
|
965
|
+
/* Every form label in the product rendered Vuetify's `text-subtitle-2`:*/
|
|
966
|
+
/* 14px/500 at 0.0071em tracking, declaring `font-family: Roboto`. */
|
|
967
|
+
/* The handoff's own label - the Create Role modal - is 12.5px/700 with*/
|
|
968
|
+
/* a 6px gap under it and the asterisk on `--err`. */
|
|
969
|
+
/* */
|
|
970
|
+
/* `margin-bottom` is deliberately NOT `!important`, so the ~40 callers*/
|
|
971
|
+
/* that already pass a Vuetify spacing utility (`mb-1`, `mb-2`) keep */
|
|
972
|
+
/* winning - those classes carry `!important` - and only the labels */
|
|
973
|
+
/* that state no spacing take the design's 6px. */
|
|
974
|
+
/* ------------------------------------------------------------------ */
|
|
975
|
+
.app-input-label {
|
|
976
|
+
font-family: var(--font-sans);
|
|
977
|
+
font-size: 12.5px;
|
|
978
|
+
font-weight: 700;
|
|
979
|
+
letter-spacing: 0;
|
|
980
|
+
color: var(--text);
|
|
981
|
+
margin-bottom: 6px;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.app-input-label__req {
|
|
985
|
+
color: var(--err);
|
|
986
|
+
}
|
package/assets/css/screens.css
CHANGED
|
@@ -739,3 +739,38 @@
|
|
|
739
739
|
height: 2px;
|
|
740
740
|
background: var(--accent);
|
|
741
741
|
}
|
|
742
|
+
|
|
743
|
+
/* ------------------------------------------------------------------ */
|
|
744
|
+
/* Toast (`components/Snackbar.vue`, 163 caller files). */
|
|
745
|
+
/* */
|
|
746
|
+
/* The handoff draws no toast at all - it is one of the gaps. So this */
|
|
747
|
+
/* is deliberately NOT an invented surface: it is the design's own */
|
|
748
|
+
/* button type scale, inner radius and menu shadow applied to */
|
|
749
|
+
/* Vuetify's snackbar. Vuetify's own default is a 4px radius, a */
|
|
750
|
+
/* Material elevation and Roboto at 14px, none of which are ours. */
|
|
751
|
+
/* */
|
|
752
|
+
/* The FILL is the `color` prop, defaulted in the component to */
|
|
753
|
+
/* `primary-button` - never `primary`, which is a foreground token. */
|
|
754
|
+
/* ------------------------------------------------------------------ */
|
|
755
|
+
.app-toast .v-snackbar__wrapper {
|
|
756
|
+
border-radius: var(--r-inner);
|
|
757
|
+
box-shadow: var(--shadow-menu);
|
|
758
|
+
font-family: var(--font-sans);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.app-toast .v-snackbar__content {
|
|
762
|
+
font-size: var(--fs-btn);
|
|
763
|
+
font-weight: 600;
|
|
764
|
+
letter-spacing: 0;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* Vuetify's text button is uppercase with wide tracking; the design has no
|
|
768
|
+
uppercase control anywhere. Named on `.v-btn` so it outweighs the default. */
|
|
769
|
+
.v-btn.app-toast__close {
|
|
770
|
+
border-radius: var(--r-inner-sm);
|
|
771
|
+
font-family: var(--font-sans);
|
|
772
|
+
font-size: var(--fs-btn);
|
|
773
|
+
font-weight: var(--fw-btn);
|
|
774
|
+
letter-spacing: 0;
|
|
775
|
+
text-transform: none;
|
|
776
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<span :class="`
|
|
3
|
-
{{ title }} <span v-if="props.required" class="
|
|
2
|
+
<span :class="`app-input-label ${props.class}`">
|
|
3
|
+
{{ title }} <span v-if="props.required" class="app-input-label__req">*</span>
|
|
4
4
|
</span>
|
|
5
5
|
</template>
|
|
6
6
|
|
|
@@ -42,6 +42,19 @@
|
|
|
42
42
|
{{ formatDateDDMMYYYYLocal(value) }}
|
|
43
43
|
</template>
|
|
44
44
|
|
|
45
|
+
<!--
|
|
46
|
+
The column had no slot, so an OBJECT `invitedBy` printed its raw JSON
|
|
47
|
+
into the cell - `{ "_id": "u-1", "name": "Unit A-12-01" }`. Never seen,
|
|
48
|
+
because the table had never drawn a row. `types/overnight-parking.d.ts`
|
|
49
|
+
declares `{ _id, name }`; every OTHER collection in `core` stores
|
|
50
|
+
`invitedBy` as a plain id/name string, and the overnight parking model
|
|
51
|
+
has no such field at all - so both shapes are read rather than betting
|
|
52
|
+
on one, and an absent value says N/A instead of nothing.
|
|
53
|
+
-->
|
|
54
|
+
<template #item.invitedBy="{ value }">
|
|
55
|
+
{{ (typeof value === "object" ? value?.name : value) || "N/A" }}
|
|
56
|
+
</template>
|
|
57
|
+
|
|
45
58
|
<!--
|
|
46
59
|
The same four words, the same four meanings - approved/expired/
|
|
47
60
|
rejected/pending map to ok/neutral/err/warn in `utils/status.ts`,
|
|
@@ -114,6 +127,15 @@
|
|
|
114
127
|
</v-card-actions>
|
|
115
128
|
</v-card>
|
|
116
129
|
</v-dialog>
|
|
130
|
+
|
|
131
|
+
<!--
|
|
132
|
+
Approve/Reject had NO failure path at all: `confirmAction` was a
|
|
133
|
+
try/finally with no catch, so a 400/403/network failure left the dialog
|
|
134
|
+
open, the spinner off and not one word on screen - the guard cannot tell
|
|
135
|
+
a saved approval from a refused one. The shared `Snackbar` is what every
|
|
136
|
+
other screen in the layer uses to say so.
|
|
137
|
+
-->
|
|
138
|
+
<Snackbar v-model="messageSnackbar" :text="snackbarMessage" :color="messageColor" />
|
|
117
139
|
</v-row>
|
|
118
140
|
</template>
|
|
119
141
|
|
|
@@ -225,7 +247,10 @@ function formatStatus(status: TOvernightParkingRequest["status"]) {
|
|
|
225
247
|
}
|
|
226
248
|
}
|
|
227
249
|
|
|
228
|
-
|
|
250
|
+
// `page` was read by the pager and never sent to the API, so page 2 re-fetched
|
|
251
|
+
// page 1 - with the API's `limit` of 10, every request past the tenth was
|
|
252
|
+
// unreachable. It is part of the query now.
|
|
253
|
+
const { data: overnightParkingRequests, pending: isPendingOvernightParkingRequests, refresh: refreshOvernightParkingRequests } = await useLazyAsyncData<TOvernightParkingRequest[]>(() => getOvernightParkingRequests({ site: props.site, status: statusFilter.value, page: page.value }), { immediate: true })
|
|
229
254
|
|
|
230
255
|
|
|
231
256
|
watch(overnightParkingRequests, (data: any) => {
|
|
@@ -257,6 +282,8 @@ function handleRowClick(data: { item?: TOvernightParkingRequest }) {
|
|
|
257
282
|
}
|
|
258
283
|
|
|
259
284
|
function handleUpdatePage(value: number) {
|
|
285
|
+
page.value = value;
|
|
286
|
+
refreshOvernightParkingRequests();
|
|
260
287
|
emits("update:page", value);
|
|
261
288
|
}
|
|
262
289
|
|
|
@@ -272,9 +299,21 @@ function closeActionDialog() {
|
|
|
272
299
|
actionDialog.open = false
|
|
273
300
|
}
|
|
274
301
|
|
|
302
|
+
const snackbarMessage = ref("")
|
|
303
|
+
const messageColor = ref("")
|
|
304
|
+
const messageSnackbar = ref(false)
|
|
305
|
+
|
|
306
|
+
function showMessage(msg: string, color: string) {
|
|
307
|
+
snackbarMessage.value = msg
|
|
308
|
+
messageColor.value = color
|
|
309
|
+
messageSnackbar.value = true
|
|
310
|
+
}
|
|
311
|
+
|
|
275
312
|
async function confirmAction() {
|
|
276
313
|
if (!actionDialog.requestId || !remarks.value.trim()) return
|
|
277
314
|
|
|
315
|
+
const verb = actionDialog.action === 'approved' ? 'approve' : 'reject'
|
|
316
|
+
|
|
278
317
|
try {
|
|
279
318
|
actionLoading.value = true
|
|
280
319
|
await updateOvernightParkingRequest(actionDialog.requestId, {
|
|
@@ -282,8 +321,14 @@ async function confirmAction() {
|
|
|
282
321
|
remarks: remarks.value.trim(),
|
|
283
322
|
})
|
|
284
323
|
actionDialog.open = false
|
|
324
|
+
// One refresh. The second, unawaited call fired the same GET a second time
|
|
325
|
+
// and could land BEFORE the first, putting the pre-update rows back.
|
|
285
326
|
await refreshOvernightParkingRequests()
|
|
286
|
-
|
|
327
|
+
showMessage(`Request ${actionDialog.action}.`, "success")
|
|
328
|
+
} catch (error: any) {
|
|
329
|
+
// The dialog stays OPEN on failure, with the remarks still typed, so the
|
|
330
|
+
// action can be retried rather than silently lost.
|
|
331
|
+
showMessage(error?.data?.message || `Failed to ${verb} this request. Please try again.`, "error")
|
|
287
332
|
} finally {
|
|
288
333
|
actionLoading.value = false
|
|
289
334
|
}
|
|
@@ -291,6 +336,9 @@ async function confirmAction() {
|
|
|
291
336
|
|
|
292
337
|
watch([statusFilter], ([newStatus], [oldStatus]) => {
|
|
293
338
|
if (newStatus === oldStatus) return
|
|
339
|
+
// A new filter is a new result set - staying on page 3 of the old one asks
|
|
340
|
+
// the API for a page that usually does not exist, and the table draws empty.
|
|
341
|
+
page.value = 1
|
|
294
342
|
refreshOvernightParkingRequests()
|
|
295
343
|
}, { immediate: false })
|
|
296
344
|
</script>
|
package/components/Snackbar.vue
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-snackbar
|
|
2
|
+
<v-snackbar
|
|
3
|
+
v-model="snackbar"
|
|
4
|
+
class="app-toast"
|
|
5
|
+
:color="props.color || 'primary-button'"
|
|
6
|
+
:z-index="100000000"
|
|
7
|
+
>
|
|
3
8
|
{{ props.text }}
|
|
4
9
|
|
|
5
10
|
<template #actions>
|
|
6
|
-
<v-btn variant="text" @click="snackbar = false">
|
|
11
|
+
<v-btn class="app-toast__close" variant="text" @click="snackbar = false">
|
|
12
|
+
Close
|
|
13
|
+
</v-btn>
|
|
7
14
|
</template>
|
|
8
15
|
</v-snackbar>
|
|
9
16
|
</template>
|
|
@@ -15,9 +22,35 @@ const props = defineProps({
|
|
|
15
22
|
type: String,
|
|
16
23
|
default: "",
|
|
17
24
|
},
|
|
25
|
+
/**
|
|
26
|
+
* A Vuetify theme colour name. `success` / `warning` / `error` / `info` are
|
|
27
|
+
* mapped onto the design palette in `utils/theme.ts` and pass through here
|
|
28
|
+
* unchanged.
|
|
29
|
+
*
|
|
30
|
+
* An UNCOLOURED toast used to land on one of two wrong surfaces, and both are
|
|
31
|
+
* corrected by the `||` in the template rather than by 163 caller files:
|
|
32
|
+
*
|
|
33
|
+
* - the prop omitted or bound to `undefined` fell to the old default
|
|
34
|
+
* `"primary"`, and `theme.ts:228` maps Vuetify's `primary` to
|
|
35
|
+
* `--accent-text` - a FOREGROUND (the link ink), never a fill. Measured:
|
|
36
|
+
* `rgb(74,110,182)` light, `rgb(120,162,244)` dark - the dark one a pale
|
|
37
|
+
* link-blue slab that matches no other accent surface in the product.
|
|
38
|
+
* - `:color=""`, which is how most callers initialise their toast state
|
|
39
|
+
* (`reactive({ show: false, message: "", color: "" })`), is not falsy to
|
|
40
|
+
* Vue's default machinery, so it reached Vuetify as an empty colour and
|
|
41
|
+
* fell through to `.v-snackbar--variant-elevated`, i.e. `surface-variant`
|
|
42
|
+
* = the NAVIGATION RAIL colour with `--text2` ink.
|
|
43
|
+
*
|
|
44
|
+
* `primary-button` is the accent FILL, and its pair `on-primary-button`
|
|
45
|
+
* supplies the white label (4.57:1 in both themes). The same correction is
|
|
46
|
+
* already on `FileInput`, `FeedbackDetail` and `BuildingUnitFormEdit`.
|
|
47
|
+
*
|
|
48
|
+
* The handoff draws no toast at all, so "an uncoloured toast is the accent
|
|
49
|
+
* fill" is our reading of a gap in the design, not a value it states.
|
|
50
|
+
*/
|
|
18
51
|
color: {
|
|
19
52
|
type: String,
|
|
20
|
-
default: "
|
|
53
|
+
default: "",
|
|
21
54
|
},
|
|
22
55
|
});
|
|
23
56
|
</script>
|
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.140",
|
|
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.",
|