@7365admin1/layer-common 4.109.0 → 4.110.0
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 +12 -0
- package/components/BuildingUnitFormEdit.vue +27 -8
- package/components/DashboardMain.vue +7 -7
- package/components/UnitTenancies.vue +291 -0
- package/composables/useBuildingUnit.ts +21 -0
- package/composables/useDashboardData.ts +2 -2
- package/package.json +1 -1
- package/utils/nav-sections.ts +4 -0
- package/utils/notification-target.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.110.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a2575af: Occupancy & tenancies (1PAM): new UnitTenancies screen (ending within 30/60/90 days or ended; Renew; End tenancy with a checked per-tenant access list), composable calls, "Tenancy Ending" notifications open Occupancy Mgmt > Tenancies; Edit Unit tab "Tenancy & Documents" with optional, ordered tenancy dates and plain help text; menu/dashboard label "Occupancy Mgmt/Management".
|
|
8
|
+
|
|
9
|
+
## 4.109.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 518f053: Task Calendar: the dashboard panel "Upcoming Events" is now "Task Calendar" (property apps); the sidebar groups the renamed menu item under PROPERTY.
|
|
14
|
+
|
|
3
15
|
## 4.109.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<v-tab value="residents">Residents/Tenants</v-tab>
|
|
12
12
|
<v-tab value="vehicles">Vehicles</v-tab>
|
|
13
13
|
<v-tab value="management">Management</v-tab>
|
|
14
|
-
<v-tab value="others">
|
|
14
|
+
<v-tab value="others">Tenancy & Documents</v-tab>
|
|
15
15
|
</v-tabs>
|
|
16
16
|
</v-toolbar>
|
|
17
17
|
<v-card-text style="max-height: 100vh; min-height: 60svh; overflow-y: auto">
|
|
@@ -193,10 +193,9 @@
|
|
|
193
193
|
<v-row>
|
|
194
194
|
<v-col cols="12" md="6" class="mt-2">
|
|
195
195
|
<v-row no-gutters>
|
|
196
|
-
<InputLabel class="text-capitalize" title="Company Name"
|
|
196
|
+
<InputLabel class="text-capitalize" title="Company Name (commercial units)" />
|
|
197
197
|
<v-col cols="12">
|
|
198
|
-
<v-text-field v-model="buildingUnit.companyName" density="comfortable"
|
|
199
|
-
:rules="[requiredRule]"></v-text-field>
|
|
198
|
+
<v-text-field v-model="buildingUnit.companyName" density="comfortable"></v-text-field>
|
|
200
199
|
</v-col>
|
|
201
200
|
</v-row>
|
|
202
201
|
</v-col>
|
|
@@ -213,21 +212,26 @@
|
|
|
213
212
|
</v-col>
|
|
214
213
|
|
|
215
214
|
<v-col cols="12">
|
|
215
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
216
|
+
If the unit is rented out, enter the tenancy dates from the tenancy agreement. You are reminded 30 days
|
|
217
|
+
and 7 days before the tenancy ends, and on the day, and can renew or end it under Occupancy Mgmt >
|
|
218
|
+
Tenancies. Leave both empty for an owner-occupied unit.
|
|
219
|
+
</p>
|
|
216
220
|
<v-row>
|
|
217
221
|
<v-col cols="12" md="6" class="mt-2">
|
|
218
222
|
<v-row no-gutters>
|
|
219
|
-
<InputLabel class="text-capitalize" title="
|
|
223
|
+
<InputLabel class="text-capitalize" title="Tenancy Start" />
|
|
220
224
|
<v-col cols="12">
|
|
221
|
-
<InputDateTimePicker ref="startDateRef" v-model="buildingUnit.leaseStart"
|
|
225
|
+
<InputDateTimePicker ref="startDateRef" v-model="buildingUnit.leaseStart" />
|
|
222
226
|
</v-col>
|
|
223
227
|
</v-row>
|
|
224
228
|
</v-col>
|
|
225
229
|
|
|
226
230
|
<v-col cols="12" md="6" class="mt-2">
|
|
227
231
|
<v-row no-gutters>
|
|
228
|
-
<InputLabel class="text-capitalize" title="
|
|
232
|
+
<InputLabel class="text-capitalize" title="Tenancy End" />
|
|
229
233
|
<v-col cols="12">
|
|
230
|
-
<InputDateTimePicker ref="endDateRef" v-model="buildingUnit.leaseEnd"
|
|
234
|
+
<InputDateTimePicker ref="endDateRef" v-model="buildingUnit.leaseEnd" />
|
|
231
235
|
</v-col>
|
|
232
236
|
</v-row>
|
|
233
237
|
</v-col>
|
|
@@ -507,6 +511,21 @@ const hasChanges = computed(() => {
|
|
|
507
511
|
});
|
|
508
512
|
|
|
509
513
|
async function submit() {
|
|
514
|
+
message.value = "";
|
|
515
|
+
// Tenancy dates are optional (an owner-occupied unit has none), but an end
|
|
516
|
+
// before its start, or a start with no end, would never remind anyone.
|
|
517
|
+
const leaseStart = String(buildingUnit.value.leaseStart || "");
|
|
518
|
+
const leaseEnd = String(buildingUnit.value.leaseEnd || "");
|
|
519
|
+
if (leaseStart && !leaseEnd) {
|
|
520
|
+
message.value = "Enter the tenancy end date, or clear the start date.";
|
|
521
|
+
tab.value = "others";
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (leaseStart && leaseEnd && leaseEnd <= leaseStart) {
|
|
525
|
+
message.value = "The tenancy end date must be after the start date.";
|
|
526
|
+
tab.value = "others";
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
510
529
|
disable.value = true;
|
|
511
530
|
try {
|
|
512
531
|
|
|
@@ -676,7 +676,7 @@
|
|
|
676
676
|
<v-col v-if="isWidgetVisible('upcomingEvents')" cols="12" lg="6">
|
|
677
677
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
678
678
|
<div class="figma-panel-header">
|
|
679
|
-
<h2>
|
|
679
|
+
<h2>Task Calendar</h2>
|
|
680
680
|
<button
|
|
681
681
|
class="figma-panel-action"
|
|
682
682
|
type="button"
|
|
@@ -2167,8 +2167,8 @@ const customizeWidgets = computed(() => {
|
|
|
2167
2167
|
},
|
|
2168
2168
|
{
|
|
2169
2169
|
key: "upcomingEvents",
|
|
2170
|
-
title: "
|
|
2171
|
-
description: "
|
|
2170
|
+
title: "Task Calendar",
|
|
2171
|
+
description: "Upcoming tasks and dates on your management team's calendar.",
|
|
2172
2172
|
},
|
|
2173
2173
|
{
|
|
2174
2174
|
key: "todayReminders",
|
|
@@ -2928,9 +2928,9 @@ const printTableSections = computed<PrintTableSection[]>(() => {
|
|
|
2928
2928
|
sections.push(
|
|
2929
2929
|
{
|
|
2930
2930
|
key: "upcomingEvents",
|
|
2931
|
-
title: "
|
|
2931
|
+
title: "Task Calendar",
|
|
2932
2932
|
columns: [
|
|
2933
|
-
{ key: "title", label: "
|
|
2933
|
+
{ key: "title", label: "Title", width: "40%", strong: true },
|
|
2934
2934
|
{ key: "subtitle", label: "Description / Subtitle", width: "60%" },
|
|
2935
2935
|
],
|
|
2936
2936
|
rows: upcomingEvents.value,
|
|
@@ -3873,9 +3873,9 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3873
3873
|
}
|
|
3874
3874
|
|
|
3875
3875
|
if (isPropertyMode.value) {
|
|
3876
|
-
wordHtml += `<div class="section-title">
|
|
3876
|
+
wordHtml += `<div class="section-title">Task Calendar</div>`;
|
|
3877
3877
|
wordHtml += `<table class="data-table">`;
|
|
3878
|
-
wordHtml += `<thead><tr><th>
|
|
3878
|
+
wordHtml += `<thead><tr><th>Title</th><th>Description / Subtitle</th></tr></thead><tbody>`;
|
|
3879
3879
|
if (upcomingEvents.value.length > 0) {
|
|
3880
3880
|
upcomingEvents.value.forEach((item) => {
|
|
3881
3881
|
wordHtml += `<tr>
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Tenancies (owner request, 1PAM meeting 2026-09-22 / build 2026-10-07).
|
|
4
|
+
|
|
5
|
+
Units whose tenancy ends soon or has ended, from the Lease Start / End on
|
|
6
|
+
the unit. The managing agent is reminded 30 days and 7 days before, and on
|
|
7
|
+
the day; from here a tenancy is renewed (new dates) or ended - ending first
|
|
8
|
+
shows exactly what access each tenant still has, and nothing is removed
|
|
9
|
+
until the MA confirms. Who may do this is decided by the server.
|
|
10
|
+
-->
|
|
11
|
+
<v-row no-gutters>
|
|
12
|
+
<v-col cols="12">
|
|
13
|
+
<PageHeader title="Tenancies" />
|
|
14
|
+
<p class="tn-intro">
|
|
15
|
+
Units whose tenancy ends within the period you choose, or has already ended. You are reminded 30 days and
|
|
16
|
+
7 days before, and on the day. <strong>Renew</strong> sets new tenancy dates. <strong>End tenancy</strong>
|
|
17
|
+
shows the tenants' access at this estate (resident app, door access, access cards, vehicles) and removes it
|
|
18
|
+
after you confirm. Tenancy dates are set on each unit (Occupancy Mgmt > Units > unit > Tenancy & Documents).
|
|
19
|
+
</p>
|
|
20
|
+
</v-col>
|
|
21
|
+
|
|
22
|
+
<v-col cols="12">
|
|
23
|
+
<AppCard class="pa-4">
|
|
24
|
+
<div class="d-flex align-center flex-wrap ga-2 mb-3">
|
|
25
|
+
<v-btn-toggle v-model="days" mandatory density="comfortable" variant="outlined" divided data-test="tn-days" @update:model-value="load">
|
|
26
|
+
<v-btn :value="30" class="text-none">Next 30 days</v-btn>
|
|
27
|
+
<v-btn :value="60" class="text-none">Next 60 days</v-btn>
|
|
28
|
+
<v-btn :value="90" class="text-none">Next 90 days</v-btn>
|
|
29
|
+
</v-btn-toggle>
|
|
30
|
+
<v-spacer />
|
|
31
|
+
<v-btn variant="text" icon="mdi-refresh" aria-label="Refresh" :loading="loading" @click="load" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<v-alert v-if="error" type="error" variant="tonal" density="compact" class="mb-3">{{ error }}</v-alert>
|
|
35
|
+
|
|
36
|
+
<div v-if="!loading && !items.length && !error" class="tn-empty" data-test="tn-empty">
|
|
37
|
+
No tenancy ends in the next {{ days }} days, and none has ended without being renewed or closed.
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div v-for="item in items" :key="item._id" class="tn-row" data-test="tn-row">
|
|
41
|
+
<div class="tn-row__main">
|
|
42
|
+
<div class="tn-row__unit">{{ item.unit }}</div>
|
|
43
|
+
<div class="tn-sub">
|
|
44
|
+
Tenancy {{ item.startLabel ? `${item.startLabel} - ` : "ends " }}{{ item.endLabel }}
|
|
45
|
+
</div>
|
|
46
|
+
<div class="tn-sub">
|
|
47
|
+
Tenants:
|
|
48
|
+
<template v-if="item.tenants.length">{{ item.tenants.map((t: any) => t.name).join(", ") }}</template>
|
|
49
|
+
<template v-else>none registered</template>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<v-chip size="small" :color="chip(item).color" variant="tonal" class="mr-2" data-test="tn-chip">{{ chip(item).text }}</v-chip>
|
|
53
|
+
<div class="tn-row__actions">
|
|
54
|
+
<v-btn size="small" variant="outlined" class="text-none" data-test="tn-renew" @click="openRenew(item)">Renew</v-btn>
|
|
55
|
+
<v-btn size="small" variant="flat" color="error" class="text-none" data-test="tn-end" @click="openEnd(item)">End tenancy</v-btn>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</AppCard>
|
|
59
|
+
</v-col>
|
|
60
|
+
</v-row>
|
|
61
|
+
|
|
62
|
+
<!-- Renew -->
|
|
63
|
+
<v-dialog v-model="renewOpen" max-width="520" persistent>
|
|
64
|
+
<v-card class="screen-modal" data-test="tn-renew-dialog">
|
|
65
|
+
<v-card-title class="d-flex align-center">
|
|
66
|
+
Renew tenancy - {{ current?.unit }}
|
|
67
|
+
<v-spacer />
|
|
68
|
+
<v-btn variant="text" icon="mdi-close" class="screen-modal__close" aria-label="Close" :disabled="saving" @click="renewOpen = false" />
|
|
69
|
+
</v-card-title>
|
|
70
|
+
<v-card-text class="screen-modal__body">
|
|
71
|
+
<p class="tn-intro">
|
|
72
|
+
Current tenancy ends {{ current?.endLabel }}. Enter the new tenancy dates from the renewed agreement. The
|
|
73
|
+
tenants keep their access, and the reminders start again for the new end date.
|
|
74
|
+
</p>
|
|
75
|
+
<label class="tn-label" for="tn-start">New tenancy start (optional)</label>
|
|
76
|
+
<input id="tn-start" v-model="renewStart" type="date" class="tn-date" data-test="tn-start" />
|
|
77
|
+
<label class="tn-label" for="tn-end">New tenancy end</label>
|
|
78
|
+
<input id="tn-end" v-model="renewEnd" type="date" class="tn-date" :min="tomorrow" data-test="tn-end-date" />
|
|
79
|
+
<div v-if="renewError" class="tn-error">{{ renewError }}</div>
|
|
80
|
+
</v-card-text>
|
|
81
|
+
<div class="screen-modal__footer">
|
|
82
|
+
<v-btn class="screen-btn-ghost" variant="outlined" :disabled="saving" @click="renewOpen = false">Cancel</v-btn>
|
|
83
|
+
<v-btn class="screen-btn-primary" variant="flat" :loading="saving" :disabled="!renewEnd" data-test="tn-renew-confirm" @click="confirmRenew">
|
|
84
|
+
Save new dates
|
|
85
|
+
</v-btn>
|
|
86
|
+
</div>
|
|
87
|
+
</v-card>
|
|
88
|
+
</v-dialog>
|
|
89
|
+
|
|
90
|
+
<!-- End tenancy -->
|
|
91
|
+
<v-dialog v-model="endOpen" max-width="640" persistent>
|
|
92
|
+
<v-card class="screen-modal" data-test="tn-end-dialog">
|
|
93
|
+
<v-card-title class="d-flex align-center">
|
|
94
|
+
End tenancy - {{ current?.unit }}
|
|
95
|
+
<v-spacer />
|
|
96
|
+
<v-btn variant="text" icon="mdi-close" class="screen-modal__close" aria-label="Close" :disabled="saving" @click="endOpen = false" />
|
|
97
|
+
</v-card-title>
|
|
98
|
+
<v-card-text class="screen-modal__body">
|
|
99
|
+
<p class="tn-intro">
|
|
100
|
+
Ending the tenancy removes each tenant's access at this estate only. Their account, and anything at other
|
|
101
|
+
estates, is not touched. The unit's owner is not affected.
|
|
102
|
+
</p>
|
|
103
|
+
<div v-if="previewLoading" class="tn-sub">Checking the tenants' access...</div>
|
|
104
|
+
<div v-else-if="previewError" class="tn-error">{{ previewError }}</div>
|
|
105
|
+
<template v-else-if="preview">
|
|
106
|
+
<div v-if="!preview.access.length" class="tn-sub">
|
|
107
|
+
No tenants are registered on this unit. Ending clears the tenancy dates only.
|
|
108
|
+
</div>
|
|
109
|
+
<div v-for="a in preview.access" :key="a.person._id" class="tn-tenant" data-test="tn-tenant">
|
|
110
|
+
<div class="tn-row__unit">{{ a.person.name }}</div>
|
|
111
|
+
<ul class="tn-list">
|
|
112
|
+
<li>Registration for this unit{{ a.appAccess ? " and resident app access" : "" }} - removed</li>
|
|
113
|
+
<li v-if="a.keepsOtherUnitHere">Also lives in another unit here - keeps resident app sign-in, door access and cards</li>
|
|
114
|
+
<li v-else-if="a.doorAccess">Door access (face / QR): {{ a.doorAccess }} - removed from the readers</li>
|
|
115
|
+
<li v-if="!a.keepsOtherUnitHere && a.accessCards">Access cards: {{ a.accessCards }} - deactivated (collect the physical card)</li>
|
|
116
|
+
<li v-if="a.vehicles.length">Vehicles: {{ a.vehicles.join(", ") }} - removed (and from the car park cameras)</li>
|
|
117
|
+
</ul>
|
|
118
|
+
</div>
|
|
119
|
+
<v-checkbox v-model="understood" density="compact" hide-details class="mt-2" data-test="tn-understood"
|
|
120
|
+
label="I have checked the list above. Remove this access and end the tenancy." />
|
|
121
|
+
</template>
|
|
122
|
+
<div v-if="endResult" class="tn-result" data-test="tn-end-result">
|
|
123
|
+
<div>{{ endResult.summary }}</div>
|
|
124
|
+
<ul v-if="endResult.failures.length" class="tn-list tn-error">
|
|
125
|
+
<li v-for="f in endResult.failures" :key="f">{{ f }} - please remove it by hand or try again.</li>
|
|
126
|
+
</ul>
|
|
127
|
+
</div>
|
|
128
|
+
</v-card-text>
|
|
129
|
+
<div class="screen-modal__footer">
|
|
130
|
+
<v-btn class="screen-btn-ghost" variant="outlined" :disabled="saving" @click="endOpen = false">{{ endResult ? "Close" : "Cancel" }}</v-btn>
|
|
131
|
+
<v-btn v-if="!endResult" color="error" variant="flat" class="text-none" :loading="saving" :disabled="!preview || !understood" data-test="tn-end-confirm" @click="confirmEnd">
|
|
132
|
+
End tenancy
|
|
133
|
+
</v-btn>
|
|
134
|
+
</div>
|
|
135
|
+
</v-card>
|
|
136
|
+
</v-dialog>
|
|
137
|
+
|
|
138
|
+
<Snackbar v-model="snack" :text="snackText" :color="snackColor" />
|
|
139
|
+
</template>
|
|
140
|
+
|
|
141
|
+
<script lang="ts" setup>
|
|
142
|
+
const props = defineProps({
|
|
143
|
+
siteId: { type: String, required: true },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const { getTenancies, renewTenancy, getEndTenancyPreview, endTenancy } = useBuildingUnit();
|
|
147
|
+
|
|
148
|
+
const days = ref(60);
|
|
149
|
+
const items = ref<Record<string, any>[]>([]);
|
|
150
|
+
const loading = ref(false);
|
|
151
|
+
const error = ref("");
|
|
152
|
+
const current = ref<Record<string, any> | null>(null);
|
|
153
|
+
const saving = ref(false);
|
|
154
|
+
|
|
155
|
+
const renewOpen = ref(false);
|
|
156
|
+
const renewStart = ref("");
|
|
157
|
+
const renewEnd = ref("");
|
|
158
|
+
const renewError = ref("");
|
|
159
|
+
|
|
160
|
+
const endOpen = ref(false);
|
|
161
|
+
const preview = ref<Record<string, any> | null>(null);
|
|
162
|
+
const previewLoading = ref(false);
|
|
163
|
+
const previewError = ref("");
|
|
164
|
+
const understood = ref(false);
|
|
165
|
+
const endResult = ref<{ summary: string; failures: string[] } | null>(null);
|
|
166
|
+
|
|
167
|
+
const snack = ref(false);
|
|
168
|
+
const snackText = ref("");
|
|
169
|
+
const snackColor = ref("success");
|
|
170
|
+
|
|
171
|
+
const tomorrow = computed(() => new Date(Date.now() + 864e5).toISOString().slice(0, 10));
|
|
172
|
+
|
|
173
|
+
const message = (e: any, fallback: string) => e?.data?.message || e?.response?._data?.message || fallback;
|
|
174
|
+
|
|
175
|
+
function chip(item: Record<string, any>) {
|
|
176
|
+
if (item.stage === "ended") return { text: "Ended", color: "error" };
|
|
177
|
+
const d = Number(item.daysLeft);
|
|
178
|
+
if (d <= 7) return { text: d <= 0 ? "Ends today" : `Ends in ${d} day${d === 1 ? "" : "s"}`, color: "warning" };
|
|
179
|
+
return { text: `Ends in ${d} days`, color: "info" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function load() {
|
|
183
|
+
if (!props.siteId) return;
|
|
184
|
+
loading.value = true;
|
|
185
|
+
error.value = "";
|
|
186
|
+
try {
|
|
187
|
+
const res = await getTenancies(props.siteId, days.value);
|
|
188
|
+
items.value = res?.items ?? [];
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
items.value = [];
|
|
191
|
+
error.value = message(e, "Tenancies could not be loaded. Please try again.");
|
|
192
|
+
} finally {
|
|
193
|
+
loading.value = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function openRenew(item: Record<string, any>) {
|
|
198
|
+
current.value = item;
|
|
199
|
+
renewStart.value = "";
|
|
200
|
+
renewEnd.value = "";
|
|
201
|
+
renewError.value = "";
|
|
202
|
+
renewOpen.value = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function confirmRenew() {
|
|
206
|
+
if (!current.value) return;
|
|
207
|
+
renewError.value = "";
|
|
208
|
+
if (renewStart.value && renewEnd.value <= renewStart.value) {
|
|
209
|
+
renewError.value = "The tenancy end date must be after the start date.";
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
saving.value = true;
|
|
213
|
+
try {
|
|
214
|
+
// Stored like the Edit Unit form stores them (local date-time, the site's time): the tenancy runs to the end of its last day.
|
|
215
|
+
await renewTenancy(current.value._id, {
|
|
216
|
+
...(renewStart.value ? { leaseStart: `${renewStart.value}T00:00` } : {}),
|
|
217
|
+
leaseEnd: `${renewEnd.value}T23:59`,
|
|
218
|
+
});
|
|
219
|
+
renewOpen.value = false;
|
|
220
|
+
say(`Tenancy for ${current.value.unit} renewed to ${renewEnd.value}.`);
|
|
221
|
+
await load();
|
|
222
|
+
} catch (e: any) {
|
|
223
|
+
renewError.value = message(e, "The tenancy could not be renewed. Please try again.");
|
|
224
|
+
} finally {
|
|
225
|
+
saving.value = false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function openEnd(item: Record<string, any>) {
|
|
230
|
+
current.value = item;
|
|
231
|
+
preview.value = null;
|
|
232
|
+
previewError.value = "";
|
|
233
|
+
understood.value = false;
|
|
234
|
+
endResult.value = null;
|
|
235
|
+
endOpen.value = true;
|
|
236
|
+
previewLoading.value = true;
|
|
237
|
+
try {
|
|
238
|
+
preview.value = await getEndTenancyPreview(item._id);
|
|
239
|
+
} catch (e: any) {
|
|
240
|
+
previewError.value = message(e, "The tenants' access could not be checked. Please try again.");
|
|
241
|
+
} finally {
|
|
242
|
+
previewLoading.value = false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function confirmEnd() {
|
|
247
|
+
if (!current.value) return;
|
|
248
|
+
saving.value = true;
|
|
249
|
+
try {
|
|
250
|
+
const r = await endTenancy(current.value._id);
|
|
251
|
+
const parts = [
|
|
252
|
+
`${r.tenants} tenant${r.tenants === 1 ? "" : "s"} removed from ${r.unit}`,
|
|
253
|
+
r.doorAccess ? `${r.doorAccess} door access` : "",
|
|
254
|
+
r.accessCards ? `${r.accessCards} access card${r.accessCards === 1 ? "" : "s"} deactivated` : "",
|
|
255
|
+
r.vehicles ? `${r.vehicles} vehicle${r.vehicles === 1 ? "" : "s"} removed` : "",
|
|
256
|
+
].filter(Boolean);
|
|
257
|
+
endResult.value = { summary: `Tenancy ended. ${parts.join(", ")}.`, failures: r.failures ?? [] };
|
|
258
|
+
if (!endResult.value.failures.length) say(endResult.value.summary);
|
|
259
|
+
await load();
|
|
260
|
+
} catch (e: any) {
|
|
261
|
+
previewError.value = message(e, "The tenancy could not be ended. Nothing was changed. Please try again.");
|
|
262
|
+
} finally {
|
|
263
|
+
saving.value = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function say(text: string, color = "success") {
|
|
268
|
+
snackText.value = text;
|
|
269
|
+
snackColor.value = color;
|
|
270
|
+
snack.value = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
watch(() => props.siteId, load, { immediate: true });
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
<style scoped>
|
|
277
|
+
.tn-intro { margin: 0 0 12px; color: var(--muted, inherit); }
|
|
278
|
+
.tn-label { display: block; font-weight: 600; margin: 10px 0 4px; }
|
|
279
|
+
.tn-date { border: 1px solid var(--line, #ccc); border-radius: 6px; padding: 8px 10px; color: inherit; background: transparent; }
|
|
280
|
+
.tn-sub { color: var(--muted, inherit); font-size: 14px; margin-top: 2px; }
|
|
281
|
+
.tn-error { color: var(--err, #b00020); margin-top: 6px; }
|
|
282
|
+
.tn-empty { padding: 24px 8px; text-align: center; color: var(--muted, inherit); }
|
|
283
|
+
.tn-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 0; border-bottom: 1px solid var(--line, #eee); }
|
|
284
|
+
.tn-row__main { flex: 1 1 260px; min-width: 0; }
|
|
285
|
+
.tn-row__unit { font-weight: 600; }
|
|
286
|
+
.tn-row__actions { display: flex; gap: 8px; }
|
|
287
|
+
.tn-tenant { padding: 10px 0; border-bottom: 1px solid var(--line, #eee); }
|
|
288
|
+
.tn-list { margin: 4px 0 0 18px; padding: 0; }
|
|
289
|
+
.tn-list li { margin: 3px 0; }
|
|
290
|
+
.tn-result { margin-top: 12px; padding: 10px 12px; border: 1px solid var(--line, #ddd); border-radius: 8px; }
|
|
291
|
+
</style>
|
|
@@ -102,7 +102,28 @@ export default function useBuildingUnit() {
|
|
|
102
102
|
directorName: "",
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
// Unit tenancies (2026-10-07): the Tenancies list, Renew and End tenancy.
|
|
106
|
+
function getTenancies(site: string, days = 60) {
|
|
107
|
+
return useNuxtApp().$api<Record<string, any>>("/api/building-units/tenancies", {
|
|
108
|
+
method: "GET",
|
|
109
|
+
query: { site, days },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function renewTenancy(id: string, body: { leaseStart?: string; leaseEnd: string }) {
|
|
113
|
+
return useNuxtApp().$api<Record<string, any>>(`/api/building-units/${id}/tenancy/renew`, { method: "POST", body });
|
|
114
|
+
}
|
|
115
|
+
function getEndTenancyPreview(id: string) {
|
|
116
|
+
return useNuxtApp().$api<Record<string, any>>(`/api/building-units/${id}/tenancy/end-preview`, { method: "GET" });
|
|
117
|
+
}
|
|
118
|
+
function endTenancy(id: string) {
|
|
119
|
+
return useNuxtApp().$api<Record<string, any>>(`/api/building-units/${id}/tenancy/end`, { method: "POST" });
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
return {
|
|
123
|
+
getTenancies,
|
|
124
|
+
renewTenancy,
|
|
125
|
+
getEndTenancyPreview,
|
|
126
|
+
endTenancy,
|
|
106
127
|
categories,
|
|
107
128
|
region,
|
|
108
129
|
getAll,
|
|
@@ -59,7 +59,7 @@ const dashboardDataMap: Record<string, ModuleData> = {
|
|
|
59
59
|
color: '#FFA726',
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
|
-
title: '
|
|
62
|
+
title: 'Occupancy Management',
|
|
63
63
|
value: 35,
|
|
64
64
|
percentage: 15.4,
|
|
65
65
|
isPositive: true,
|
|
@@ -95,7 +95,7 @@ const dashboardDataMap: Record<string, ModuleData> = {
|
|
|
95
95
|
'Feedback',
|
|
96
96
|
'Work Orders',
|
|
97
97
|
'Vehicle Management',
|
|
98
|
-
'
|
|
98
|
+
'Occupancy Management',
|
|
99
99
|
'Visitor Management',
|
|
100
100
|
'Incident Reports',
|
|
101
101
|
'Guest Management',
|
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.
|
|
5
|
+
"version": "4.110.0",
|
|
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.",
|
package/utils/nav-sections.ts
CHANGED
|
@@ -57,6 +57,9 @@ export const NAV_SECTIONS: Record<string, string> = {
|
|
|
57
57
|
|
|
58
58
|
// PROPERTY
|
|
59
59
|
"Vehicle Mgmt": "PROPERTY",
|
|
60
|
+
// Renamed "Occupancy Mgmt" for the client (1PAM, 2026-10-07); the old title
|
|
61
|
+
// stays mapped so an app still pinned to an older menu groups the same.
|
|
62
|
+
"Occupancy Mgmt": "PROPERTY",
|
|
60
63
|
"Building Mgmt": "PROPERTY",
|
|
61
64
|
"Visitor Mgmt": "PROPERTY",
|
|
62
65
|
"Pass & Key Mgmt": "PROPERTY",
|
|
@@ -65,6 +68,7 @@ export const NAV_SECTIONS: Record<string, string> = {
|
|
|
65
68
|
"Document Mgmt": "PROPERTY",
|
|
66
69
|
"Access Mgmt": "PROPERTY",
|
|
67
70
|
"Event Mgmt": "PROPERTY",
|
|
71
|
+
"Task Calendar": "PROPERTY",
|
|
68
72
|
"Emergency Contacts": "PROPERTY",
|
|
69
73
|
"Online Billing": "PROPERTY",
|
|
70
74
|
"SOA Mgmt": "PROPERTY",
|
|
@@ -188,6 +188,7 @@ const SITE_PAGES: Record<string, TSitePage[]> = {
|
|
|
188
188
|
facilityBooking: [{ apps: ["property-management"], page: () => "facility-booking" }],
|
|
189
189
|
onlineForm: [{ apps: ["property-management"], page: () => "online-forms" }],
|
|
190
190
|
documentExpiry: [{ apps: ["property-management"], page: () => "document-mgmt" }],
|
|
191
|
+
tenancyExpiry: [{ apps: ["property-management"], page: () => "building-mgmt/tenancies" }],
|
|
191
192
|
emergencyContact: [{ apps: ["property-management"], page: () => "emergency-contacts" }],
|
|
192
193
|
incidentReport: [{ apps: ["security", "property-management"], page: withId("incident-reports") }],
|
|
193
194
|
visitors: [
|