@7365admin1/layer-common 4.2.3 → 4.2.5-staging.270
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/CHANGELOG.md +28 -4
- package/components/BulletinBoardForm.vue +0 -1
- package/components/Input/DateTimePicker.vue +78 -155
- package/components/VehicleForm.vue +94 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 23b3b39: Fix the shared date/time field: a date the user types or picks now reaches the payload.
|
|
8
|
+
|
|
9
|
+
`Input/DateTimePicker` displayed a read-only Vuetify text field over a hidden native input. Typing went into Vuetify's own copy of the value — it displayed, it passed the required rule, and both models stayed empty, so `startDate: ""` and `endDate: ""` were submitted. Picking from the calendar drove the invisible input, whose segments never completed, and set nothing. The field is now the native input itself, two-way bound.
|
|
10
|
+
|
|
11
|
+
Affects every form that takes a date: bulletin board, people, vehicle season pass, building unit lease and the cleaning schedule filters.
|
|
12
|
+
|
|
13
|
+
- 05428e8: Send vehicle season pass dates as real instants, not zoneless wall-clock strings
|
|
14
|
+
|
|
15
|
+
A pass entered as expiring 05/12/2026 at 6:45 pm was stored as 06/12/2026 at
|
|
16
|
+
2:45 am. The season pass form sent `start` and `end` as `YYYY-MM-DDTHH:mm` with
|
|
17
|
+
no timezone, so the server parsed them in its own zone (UTC) while the browser
|
|
18
|
+
rendered the result back in SGT: exactly +8h on both fields, with the expiry
|
|
19
|
+
rolling into the next day.
|
|
20
|
+
|
|
21
|
+
Both date fields now bind the picker's `utc` model, the same binding the
|
|
22
|
+
bulletin board and people forms already use, and the subscription preset buttons
|
|
23
|
+
write a UTC instant instead of a wall clock. The picker also prefers a value
|
|
24
|
+
written by the parent over its own copy of the last typed one, so a preset
|
|
25
|
+
clicked after typing no longer leaves the old date on screen.
|
|
26
|
+
|
|
3
27
|
## 4.2.3
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
|
@@ -468,11 +492,11 @@
|
|
|
468
492
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
469
493
|
own rule from two endpoints the console already calls, unprojected:
|
|
470
494
|
|
|
471
|
-
|
|
472
|
-
|
|
495
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
496
|
+
GET /api/roles/id/:role that membership's role document
|
|
473
497
|
|
|
474
|
-
|
|
475
|
-
|
|
498
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
499
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
476
500
|
|
|
477
501
|
`role.default` is the marker because it is the only property of a platform
|
|
478
502
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -193,7 +193,6 @@ const today = computed(() => {
|
|
|
193
193
|
const yyyy = d.getFullYear();
|
|
194
194
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
195
195
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
196
|
-
console.log("${yyyy}-${mm}-${dd}", `${yyyy}-${mm}-${dd}`);
|
|
197
196
|
return `${yyyy}-${mm}-${dd}`;
|
|
198
197
|
});
|
|
199
198
|
|
|
@@ -1,42 +1,54 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<template #append-inner>
|
|
15
|
-
<v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
|
|
16
|
-
</template>
|
|
17
|
-
</v-text-field>
|
|
18
|
-
<div class="w-100 d-flex align-end ga-3 hidden-input">
|
|
19
|
-
<input
|
|
20
|
-
ref="dateInput"
|
|
21
|
-
:type="inputType"
|
|
22
|
-
v-model="dateTime"
|
|
23
|
-
:min="minValue"
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
2
|
+
<v-text-field
|
|
3
|
+
v-bind="$attrs"
|
|
4
|
+
ref="dateTimePickerRef"
|
|
5
|
+
class="filter-field"
|
|
6
|
+
v-model="fieldValue"
|
|
7
|
+
:type="inputType"
|
|
8
|
+
:label="placeholder || undefined"
|
|
9
|
+
:min="minValue"
|
|
10
|
+
:rules="rules"
|
|
11
|
+
persistent-placeholder
|
|
12
|
+
autocomplete="off"
|
|
13
|
+
/>
|
|
27
14
|
</template>
|
|
28
15
|
|
|
29
16
|
<script setup lang="ts">
|
|
30
|
-
import { ref, computed
|
|
17
|
+
import { ref, computed } from "vue";
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The field the user edits IS the native date input now.
|
|
21
|
+
*
|
|
22
|
+
* It used to be two elements: a read-only Vuetify text field for show, and a
|
|
23
|
+
* zero-height `opacity: 0` native input behind it that `showPicker()` was
|
|
24
|
+
* called on. That arrangement broke every way a user can enter a date:
|
|
25
|
+
*
|
|
26
|
+
* - TYPING went into Vuetify's own internal copy of the value (the text field
|
|
27
|
+
* was bound with `:model-value` and nothing listened for the update), so the
|
|
28
|
+
* typed date DISPLAYED and PASSED the required-rule while both models here
|
|
29
|
+
* stayed empty - `startDate: ""` went to the API, and nothing could ever be
|
|
30
|
+
* Upcoming.
|
|
31
|
+
* - PICKING a date drove the invisible input. A partly-filled
|
|
32
|
+
* `datetime-local` has an empty value until every segment is set, and the
|
|
33
|
+
* segments were invisible, so the picker closed and set nothing.
|
|
34
|
+
* - An End Date that was typed stayed invalid for the same reason as the
|
|
35
|
+
* first point, so an announcement with a real end date could not be created
|
|
36
|
+
* at all.
|
|
37
|
+
*
|
|
38
|
+
* One visible native input has the browser's own picker, its own typing, and a
|
|
39
|
+
* real `v-model`, which is all three fixes.
|
|
40
|
+
*/
|
|
31
41
|
|
|
32
42
|
const prop = defineProps({
|
|
33
43
|
rules: {
|
|
34
44
|
type: Array as PropType<Array<any>>,
|
|
35
45
|
default: () => [],
|
|
36
46
|
},
|
|
47
|
+
// Rendered as the field's label: a native date input always draws its own
|
|
48
|
+
// dd/mm/yyyy segments, so a placeholder would never be seen.
|
|
37
49
|
placeholder: {
|
|
38
50
|
type: String,
|
|
39
|
-
default: "
|
|
51
|
+
default: "",
|
|
40
52
|
},
|
|
41
53
|
dateOnly: {
|
|
42
54
|
type: Boolean,
|
|
@@ -48,11 +60,10 @@ const prop = defineProps({
|
|
|
48
60
|
},
|
|
49
61
|
});
|
|
50
62
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const dateTimeUTC = defineModel<string | null>("utc", { default: null }); // UTC format
|
|
63
|
+
const dateTime = defineModel<string | null>({ default: null }); // 2025-10-10T13:09, local
|
|
64
|
+
const dateTimeUTC = defineModel<string | null>("utc", { default: null }); // ISO 8601, UTC
|
|
54
65
|
|
|
55
|
-
const
|
|
66
|
+
const dateTimePickerRef = ref<HTMLInputElement | null>(null);
|
|
56
67
|
const inputType = computed(() => (prop.dateOnly ? "date" : "datetime-local"));
|
|
57
68
|
|
|
58
69
|
const minValue = computed(() => {
|
|
@@ -62,148 +73,60 @@ const minValue = computed(() => {
|
|
|
62
73
|
}
|
|
63
74
|
return prop.min;
|
|
64
75
|
});
|
|
65
|
-
const inputPlaceholder = computed(
|
|
66
|
-
() =>
|
|
67
|
-
prop.placeholder ||
|
|
68
|
-
(prop.dateOnly ? "MM/DD/YYYY" : "DD/MM/YYYY, HH:MM AM/PM")
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const dateInput = ref<HTMLInputElement | null>(null);
|
|
72
|
-
const dateTimePickerRef = ref<HTMLInputElement | null>(null);
|
|
73
|
-
let nativeInput: HTMLInputElement | null = null;
|
|
74
|
-
|
|
75
|
-
const isInitialLoad = ref(true);
|
|
76
|
-
|
|
77
|
-
function handleNativeInputClick(e: MouseEvent) {
|
|
78
|
-
e.stopPropagation();
|
|
79
|
-
openDatePicker();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function openDatePicker() {
|
|
83
|
-
setTimeout(() => {
|
|
84
|
-
dateInput.value?.showPicker?.();
|
|
85
|
-
}, 0);
|
|
86
|
-
}
|
|
87
76
|
|
|
88
77
|
function validate() {
|
|
89
78
|
(dateTimePickerRef.value as any)?.validate();
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
if (!dateStr) return "";
|
|
81
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
94
82
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const year = date.getFullYear();
|
|
101
|
-
|
|
102
|
-
if (prop.dateOnly) {
|
|
103
|
-
return `${day}/${month}/${year}`;
|
|
83
|
+
// What the native input accepts: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm`, local time.
|
|
84
|
+
function toInputValue(value: string | null): string {
|
|
85
|
+
if (!value) return "";
|
|
86
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
87
|
+
return prop.dateOnly ? value : `${value}T00:00`;
|
|
104
88
|
}
|
|
105
|
-
|
|
106
|
-
let hours = date.getHours();
|
|
107
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
108
|
-
|
|
109
|
-
const ampm = hours >= 12 ? "PM" : "AM";
|
|
110
|
-
hours = hours % 12;
|
|
111
|
-
hours = hours ? hours : 12;
|
|
112
|
-
|
|
113
|
-
const formattedTime = `${String(hours).padStart(2, "0")}:${minutes} ${ampm}`;
|
|
114
|
-
|
|
115
|
-
return `${day}/${month}/${year}, ${formattedTime}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function toDateOnlyInputValue(dateStr: string): string {
|
|
119
|
-
if (!dateStr) return "";
|
|
120
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr;
|
|
121
|
-
const date = new Date(dateStr);
|
|
89
|
+
const date = new Date(value);
|
|
122
90
|
if (Number.isNaN(date.getTime())) return "";
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
91
|
+
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
92
|
+
return prop.dateOnly
|
|
93
|
+
? day
|
|
94
|
+
: `${day}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
127
95
|
}
|
|
128
96
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const localDate = new Date(dateDefault);
|
|
138
|
-
dateTimeUTC.value = localDate.toISOString();
|
|
139
|
-
}
|
|
140
|
-
} else if (dateUTC) {
|
|
141
|
-
dateTimeFormattedReadOnly.value = convertToReadableFormat(dateUTC);
|
|
142
|
-
if (prop.dateOnly) {
|
|
143
|
-
dateTime.value = toDateOnlyInputValue(dateUTC);
|
|
144
|
-
} else {
|
|
145
|
-
const localDate = new Date(dateUTC);
|
|
146
|
-
dateTime.value = formatDateISO8601(localDate);
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
dateTimeFormattedReadOnly.value = null;
|
|
150
|
-
}
|
|
97
|
+
// What the API stores: a date-only field stays `YYYY-MM-DD`, everything else
|
|
98
|
+
// becomes UTC.
|
|
99
|
+
function toUTCValue(value: string | null): string | null {
|
|
100
|
+
if (!value) return null;
|
|
101
|
+
if (prop.dateOnly) return toInputValue(value).slice(0, 10) || null;
|
|
102
|
+
const date = new Date(value);
|
|
103
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
104
|
+
return date.toISOString();
|
|
151
105
|
}
|
|
152
106
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
dateTimeUTC.value
|
|
166
|
-
} else {
|
|
167
|
-
const localDate = new Date(dateVal);
|
|
168
|
-
dateTimeUTC.value = localDate.toISOString();
|
|
107
|
+
// The one value the field is bound to. A getter and a setter instead of a pair
|
|
108
|
+
// of watchers: no initial-load flag, no chance of the two models fighting each
|
|
109
|
+
// other, and it behaves the same on the server as in the browser.
|
|
110
|
+
const fieldValue = computed({
|
|
111
|
+
get: () => {
|
|
112
|
+
// The two models only agree when the setter below wrote them together. When
|
|
113
|
+
// a PARENT writes one of them - the season pass dialog's subscription
|
|
114
|
+
// buttons write the UTC one - the other still holds this component's copy
|
|
115
|
+
// of whatever was in the field before, and reading that copy first would
|
|
116
|
+
// leave the field showing a date the form is no longer holding.
|
|
117
|
+
// Disagreement means the parent wrote, so the parent wins.
|
|
118
|
+
if (dateTimeUTC.value && toUTCValue(dateTime.value) !== dateTimeUTC.value) {
|
|
119
|
+
return toInputValue(dateTimeUTC.value);
|
|
169
120
|
}
|
|
121
|
+
return toInputValue(dateTime.value || dateTimeUTC.value);
|
|
170
122
|
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
watch(
|
|
175
|
-
dateTimeUTC,
|
|
176
|
-
() => {
|
|
177
|
-
handleInitialDate();
|
|
123
|
+
set: (value: string | null) => {
|
|
124
|
+
dateTime.value = value || null;
|
|
125
|
+
dateTimeUTC.value = toUTCValue(value);
|
|
178
126
|
},
|
|
179
|
-
{ immediate: true }
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
onMounted(async () => {
|
|
183
|
-
await nextTick();
|
|
184
|
-
isInitialLoad.value = false;
|
|
185
|
-
// Wait until Vuetify renders its internal input
|
|
186
|
-
nativeInput = (dateTimePickerRef.value as any)?.$el?.querySelector(
|
|
187
|
-
"input"
|
|
188
|
-
);
|
|
189
|
-
if (nativeInput) {
|
|
190
|
-
nativeInput.addEventListener("click", handleNativeInputClick);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
onBeforeUnmount(() => {
|
|
195
|
-
nativeInput?.removeEventListener("click", handleNativeInputClick);
|
|
196
|
-
nativeInput = null;
|
|
197
127
|
});
|
|
198
128
|
|
|
199
129
|
defineExpose({
|
|
200
130
|
validate,
|
|
201
131
|
});
|
|
202
132
|
</script>
|
|
203
|
-
|
|
204
|
-
<style scoped>
|
|
205
|
-
.hidden-input {
|
|
206
|
-
opacity: 0;
|
|
207
|
-
height: 0;
|
|
208
|
-
}
|
|
209
|
-
</style>
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
<InputLabel class="text-capitalize" title="Block" required />
|
|
59
59
|
<v-autocomplete
|
|
60
60
|
v-model="vehicle.block"
|
|
61
|
-
:items="
|
|
61
|
+
:items="blockItems"
|
|
62
62
|
autocomplete="off"
|
|
63
63
|
:key="
|
|
64
64
|
blockListDataPending ? 'block-list-pending' : 'block-list-ready'
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<InputLabel class="text-capitalize" title="Level" required />
|
|
77
77
|
<v-autocomplete
|
|
78
78
|
v-model="vehicle.level"
|
|
79
|
-
:items="
|
|
79
|
+
:items="levelItems"
|
|
80
80
|
autocomplete="off"
|
|
81
81
|
:key="
|
|
82
82
|
levelListDataPending ? 'level-list-pending' : 'level-list-ready'
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
<InputLabel class="text-capitalize" title="Unit" required />
|
|
94
94
|
<v-autocomplete
|
|
95
95
|
v-model="vehicle.unit"
|
|
96
|
-
:items="
|
|
96
|
+
:items="unitItems"
|
|
97
97
|
autocomplete="off"
|
|
98
98
|
:key="
|
|
99
99
|
unitListDataPending ? 'unit-list-pending' : 'unit-list-ready'
|
|
@@ -268,7 +268,7 @@
|
|
|
268
268
|
/>
|
|
269
269
|
<InputDateTimePicker
|
|
270
270
|
ref="startDateRef"
|
|
271
|
-
v-model="vehicle.start"
|
|
271
|
+
v-model:utc="vehicle.start"
|
|
272
272
|
:rules="[validStartDateRule]"
|
|
273
273
|
/>
|
|
274
274
|
</v-col>
|
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
/>
|
|
282
282
|
<InputDateTimePicker
|
|
283
283
|
ref="expiryDateRef"
|
|
284
|
-
v-model="vehicle.end"
|
|
284
|
+
v-model:utc="vehicle.end"
|
|
285
285
|
:rules="[validExpiryDateRule]"
|
|
286
286
|
/>
|
|
287
287
|
</v-col>
|
|
@@ -439,7 +439,7 @@ const prop = defineProps({
|
|
|
439
439
|
},
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
-
const { requiredRule,
|
|
442
|
+
const { requiredRule, debounce } = useUtils();
|
|
443
443
|
const {
|
|
444
444
|
addVehicle,
|
|
445
445
|
getCustomSeasonPassTypes,
|
|
@@ -532,10 +532,78 @@ const shouldShowField = (fieldKey: string): boolean => {
|
|
|
532
532
|
return visibleFields?.includes(fieldKey);
|
|
533
533
|
};
|
|
534
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Block / Level / Unit are ids bound to autocompletes whose `:items` come from
|
|
537
|
+
* the site's own lists. Vuetify renders the RAW model value when it is not in
|
|
538
|
+
* that list, which is how the Season Pass edit dialog came to show
|
|
539
|
+
* `6a31fd37917fb18ef1a21a0a` in all three fields instead of names.
|
|
540
|
+
*
|
|
541
|
+
* Two different causes end in the same place, and neither is fixable in the
|
|
542
|
+
* repository: the site list has simply not arrived yet, and - measured on
|
|
543
|
+
* staging - a stored unit id that points at a building unit which has since
|
|
544
|
+
* been DELETED, so it can NEVER appear in the list however the row is fetched.
|
|
545
|
+
*
|
|
546
|
+
* So the fix is here, at the display layer: an id with no name gets an explicit
|
|
547
|
+
* item saying why there is no name. A Mongo id is not a name, and a blank field
|
|
548
|
+
* with no explanation is not better than one.
|
|
549
|
+
*
|
|
550
|
+
* The stored value is left untouched - the field still submits the same id, so
|
|
551
|
+
* saving an unrelated edit does not silently rewrite someone's unit.
|
|
552
|
+
*/
|
|
553
|
+
function withUnresolved(
|
|
554
|
+
items: TDefaultOptionObj[],
|
|
555
|
+
value: unknown,
|
|
556
|
+
pending: boolean,
|
|
557
|
+
noun: string
|
|
558
|
+
): TDefaultOptionObj[] {
|
|
559
|
+
const id =
|
|
560
|
+
typeof value === "string" ? value : ((value as any)?._id ?? "");
|
|
561
|
+
if (!id || items.some((item) => item.value === id)) return items;
|
|
562
|
+
|
|
563
|
+
return [
|
|
564
|
+
{
|
|
565
|
+
title: pending
|
|
566
|
+
? `Loading ${noun}…`
|
|
567
|
+
: `This ${noun} is no longer on the site list (${id})`,
|
|
568
|
+
value: id,
|
|
569
|
+
},
|
|
570
|
+
...items,
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const blockItems = computed(() =>
|
|
575
|
+
withUnresolved(
|
|
576
|
+
blocksArray.value,
|
|
577
|
+
vehicle.block,
|
|
578
|
+
blockListDataPending.value,
|
|
579
|
+
"block"
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const levelItems = computed(() =>
|
|
584
|
+
withUnresolved(
|
|
585
|
+
levelsArray.value,
|
|
586
|
+
vehicle.level,
|
|
587
|
+
levelListDataPending.value,
|
|
588
|
+
"level"
|
|
589
|
+
)
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const unitItems = computed(() =>
|
|
593
|
+
withUnresolved(
|
|
594
|
+
unitsArray.value,
|
|
595
|
+
vehicle.unit,
|
|
596
|
+
unitListDataPending.value,
|
|
597
|
+
"unit"
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
|
|
535
601
|
const searchUnitName = computed(() => {
|
|
602
|
+
// Same rule as the fields: never put a raw id in front of a person. The
|
|
603
|
+
// dialog title used to fall through to `vehicle.unit`, so a deleted unit
|
|
604
|
+
// titled the panel with its ObjectId.
|
|
536
605
|
return (
|
|
537
|
-
unitsArray.value.find((u) => u.value === vehicle.unit)?.title ||
|
|
538
|
-
vehicle.unit
|
|
606
|
+
unitsArray.value.find((u) => u.value === vehicle.unit)?.title || "-"
|
|
539
607
|
);
|
|
540
608
|
});
|
|
541
609
|
|
|
@@ -881,15 +949,20 @@ async function submit() {
|
|
|
881
949
|
|
|
882
950
|
// }
|
|
883
951
|
|
|
952
|
+
/*
|
|
953
|
+
* The subscription buttons write the same thing the picker writes: a real
|
|
954
|
+
* instant.
|
|
955
|
+
*
|
|
956
|
+
* `formatDateISO8601` returns `YYYY-MM-DDTHH:mm` with no zone - the browser's
|
|
957
|
+
* WALL CLOCK, not a moment in time. Sent to the API as-is, the server parsed it
|
|
958
|
+
* in its own zone (UTC) and a pass entered as 05/12/2026 6:45 pm came back as
|
|
959
|
+
* 06/12/2026 2:45 am. These two fields now carry UTC end to end, so the preset
|
|
960
|
+
* and the picker cannot disagree about what "now" meant.
|
|
961
|
+
*/
|
|
884
962
|
watch(selectedSubscriptionDuration, (duration) => {
|
|
885
|
-
const dateNowISO =
|
|
886
|
-
const weeksLaterISO = (week: number) =>
|
|
887
|
-
|
|
888
|
-
const dateWeekLater = new Date(
|
|
889
|
-
now.getTime() + week * 7 * 24 * 60 * 60 * 1000
|
|
890
|
-
);
|
|
891
|
-
return formatDateISO8601(dateWeekLater);
|
|
892
|
-
};
|
|
963
|
+
const dateNowISO = new Date().toISOString();
|
|
964
|
+
const weeksLaterISO = (week: number) =>
|
|
965
|
+
new Date(Date.now() + week * 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
893
966
|
|
|
894
967
|
if (duration === "custom") {
|
|
895
968
|
vehicle.start = "";
|
|
@@ -1042,10 +1115,12 @@ const resetVehicleDetails = () => {
|
|
|
1042
1115
|
};
|
|
1043
1116
|
|
|
1044
1117
|
function handleCloseMatchDialog() {
|
|
1118
|
+
// Close only. This used to null `vehicle.unit` whenever the panel had found
|
|
1119
|
+
// matches, so backing out of the picker with the header X wiped a Unit the
|
|
1120
|
+
// user had chosen BEFORE opening it and painted the field required-in-red.
|
|
1121
|
+
// The panel's own close button never did that; the two now agree. Backing out
|
|
1122
|
+
// of a picker is not a decision to discard what you already had.
|
|
1045
1123
|
showMatchingPeopleDialog.value = false;
|
|
1046
|
-
if (matchingPeople.value.length > 0) {
|
|
1047
|
-
vehicle.unit = null;
|
|
1048
|
-
}
|
|
1049
1124
|
}
|
|
1050
1125
|
|
|
1051
1126
|
onMounted(() => {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.2.
|
|
5
|
+
"version": "4.2.5-staging.270",
|
|
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.",
|