@7365admin1/layer-common 3.1.4-staging.58 → 3.2.1-staging.59
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 +6 -0
- package/components/CreateSubsciptionPlan.vue +376 -0
- package/components/DashboardMain.vue +7 -6
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +6 -2
- package/components/PlanActionsMenu.vue +33 -0
- package/components/PlanStatusBadge.vue +26 -0
- package/components/SubscriptionPlanTable.vue +362 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/package.json +1 -1
- package/types/subscription.ts +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="pa-2">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<v-card-title class="d-flex align-center justify-space-between pb-2">
|
|
5
|
+
<span class="text-h6 font-weight-bold">
|
|
6
|
+
{{ isEditing ? "Edit Plan" : "Create New Plan" }}
|
|
7
|
+
</span>
|
|
8
|
+
|
|
9
|
+
<v-btn
|
|
10
|
+
icon="mdi-close"
|
|
11
|
+
variant="text"
|
|
12
|
+
density="comfortable"
|
|
13
|
+
aria-label="Close"
|
|
14
|
+
@click="$emit('cancel')"
|
|
15
|
+
/>
|
|
16
|
+
</v-card-title>
|
|
17
|
+
|
|
18
|
+
<v-divider />
|
|
19
|
+
|
|
20
|
+
<!-- Form Content -->
|
|
21
|
+
<v-form ref="formRef" class="my-4" @submit.prevent="submit">
|
|
22
|
+
<v-card-text class="pa-2 max-h-70vh overflow-y-auto">
|
|
23
|
+
<!-- Plan Details Section -->
|
|
24
|
+
<div class="text-subtitle-2 font-weight-bold mb-3 text-grey-darken-3">
|
|
25
|
+
Plan Details
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Plan Name -->
|
|
29
|
+
<v-text-field
|
|
30
|
+
v-model="form.name"
|
|
31
|
+
label="Plan Name"
|
|
32
|
+
placeholder="e.g., Business"
|
|
33
|
+
variant="outlined"
|
|
34
|
+
density="compact"
|
|
35
|
+
:error-messages="errors.name"
|
|
36
|
+
class="mb-3"
|
|
37
|
+
required
|
|
38
|
+
>
|
|
39
|
+
<template #label>
|
|
40
|
+
Plan Name <span class="text-error">*</span>
|
|
41
|
+
</template>
|
|
42
|
+
</v-text-field>
|
|
43
|
+
|
|
44
|
+
<!-- Description -->
|
|
45
|
+
<v-textarea
|
|
46
|
+
v-model="form.description"
|
|
47
|
+
label="Description (optional)"
|
|
48
|
+
placeholder="Enter description"
|
|
49
|
+
variant="outlined"
|
|
50
|
+
density="compact"
|
|
51
|
+
rows="2"
|
|
52
|
+
auto-grow
|
|
53
|
+
class="mb-3"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<!-- Max Seats -->
|
|
57
|
+
<v-text-field
|
|
58
|
+
v-model.number="form.maxSeats"
|
|
59
|
+
type="number"
|
|
60
|
+
min="0"
|
|
61
|
+
variant="outlined"
|
|
62
|
+
density="compact"
|
|
63
|
+
class="mb-4"
|
|
64
|
+
required
|
|
65
|
+
>
|
|
66
|
+
<template #label>
|
|
67
|
+
Max seats <span class="text-error">*</span>
|
|
68
|
+
</template>
|
|
69
|
+
</v-text-field>
|
|
70
|
+
|
|
71
|
+
<v-divider class="my-4" />
|
|
72
|
+
|
|
73
|
+
<!-- Pricing Section -->
|
|
74
|
+
<div class="text-subtitle-2 font-weight-bold mb-3 text-grey-darken-3">
|
|
75
|
+
Pricing
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Plan Type -->
|
|
79
|
+
<v-select
|
|
80
|
+
v-model="form.planType"
|
|
81
|
+
:items="planTypeOptions"
|
|
82
|
+
variant="outlined"
|
|
83
|
+
density="compact"
|
|
84
|
+
class="mb-3"
|
|
85
|
+
>
|
|
86
|
+
<template #label>
|
|
87
|
+
Subscription plan type <span class="text-error">*</span>
|
|
88
|
+
</template>
|
|
89
|
+
</v-select>
|
|
90
|
+
|
|
91
|
+
<!-- Billing Cycle -->
|
|
92
|
+
<v-select
|
|
93
|
+
v-if="form.planType !== 'free_bundle'"
|
|
94
|
+
v-model="form.billingCycle"
|
|
95
|
+
:items="billingCycleOptions"
|
|
96
|
+
placeholder="Select billing cycle"
|
|
97
|
+
variant="outlined"
|
|
98
|
+
density="compact"
|
|
99
|
+
class="mb-3"
|
|
100
|
+
>
|
|
101
|
+
<template #label>
|
|
102
|
+
Billing cycle <span class="text-error">*</span>
|
|
103
|
+
</template>
|
|
104
|
+
</v-select>
|
|
105
|
+
|
|
106
|
+
<!-- Bundle Applications (Chip Toggle) -->
|
|
107
|
+
<div v-if="form.planType !== 'custom_apps'" class="mb-4">
|
|
108
|
+
<div class="text-caption font-weight-medium text-grey-darken-2 mb-2">
|
|
109
|
+
Included Applications
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<v-chip-group
|
|
113
|
+
v-model="form.bundleApps"
|
|
114
|
+
column
|
|
115
|
+
multiple
|
|
116
|
+
selected-class="bg-grey-darken-3 text-white"
|
|
117
|
+
>
|
|
118
|
+
<v-chip
|
|
119
|
+
v-for="app in AVAILABLE_APPLICATIONS"
|
|
120
|
+
:key="app.id"
|
|
121
|
+
:value="app.id"
|
|
122
|
+
filter
|
|
123
|
+
variant="outlined"
|
|
124
|
+
size="small"
|
|
125
|
+
>
|
|
126
|
+
{{ app.name }}
|
|
127
|
+
</v-chip>
|
|
128
|
+
</v-chip-group>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Bundle Price -->
|
|
132
|
+
<v-text-field
|
|
133
|
+
v-if="form.planType === 'paid_bundle'"
|
|
134
|
+
v-model.number="form.bundlePrice"
|
|
135
|
+
type="number"
|
|
136
|
+
min="0"
|
|
137
|
+
placeholder="0"
|
|
138
|
+
prefix="$"
|
|
139
|
+
:suffix="form.billingCycle === 'annually' ? '/yr' : '/mo'"
|
|
140
|
+
variant="outlined"
|
|
141
|
+
density="compact"
|
|
142
|
+
class="mb-3"
|
|
143
|
+
>
|
|
144
|
+
<template #label>
|
|
145
|
+
Price <span class="text-error">*</span>
|
|
146
|
+
</template>
|
|
147
|
+
</v-text-field>
|
|
148
|
+
|
|
149
|
+
<!-- Custom Applications (List Checkbox) -->
|
|
150
|
+
<div v-if="form.planType === 'custom_apps'" class="mb-4">
|
|
151
|
+
<div class="text-caption font-weight-medium text-grey-darken-2 mb-1">
|
|
152
|
+
Included Applications <span class="text-error">*</span>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<v-card variant="outlined" class="pa-1 mb-3">
|
|
156
|
+
<v-list density="compact" class="py-0">
|
|
157
|
+
<v-list-item
|
|
158
|
+
v-for="app in AVAILABLE_APPLICATIONS"
|
|
159
|
+
:key="app.id"
|
|
160
|
+
class="px-2"
|
|
161
|
+
>
|
|
162
|
+
<template #prepend>
|
|
163
|
+
<v-checkbox-btn
|
|
164
|
+
:model-value="form.customApps.includes(app.id)"
|
|
165
|
+
density="compact"
|
|
166
|
+
@update:model-value="toggleApp(form.customApps, app.id)"
|
|
167
|
+
/>
|
|
168
|
+
</template>
|
|
169
|
+
|
|
170
|
+
<v-list-item-title class="text-body-2">
|
|
171
|
+
{{ app.name }}
|
|
172
|
+
</v-list-item-title>
|
|
173
|
+
|
|
174
|
+
<template #append>
|
|
175
|
+
<span class="text-caption text-grey">
|
|
176
|
+
${{ form.billingCycle === 'annually' ? app.yearlyPrice : app.monthlyPrice }}/{{ form.billingCycle === 'annually' ? 'yr' : 'mo' }}
|
|
177
|
+
</span>
|
|
178
|
+
</template>
|
|
179
|
+
</v-list-item>
|
|
180
|
+
</v-list>
|
|
181
|
+
</v-card>
|
|
182
|
+
|
|
183
|
+
<!-- Custom Total Price Display -->
|
|
184
|
+
<v-sheet
|
|
185
|
+
color="grey-lighten-4"
|
|
186
|
+
rounded
|
|
187
|
+
class="pa-3 d-flex align-center justify-space-between mb-1"
|
|
188
|
+
>
|
|
189
|
+
<span class="text-caption text-grey-darken-2">
|
|
190
|
+
Total price {{ form.billingCycle === 'annually' ? '/ year' : '/ month' }}
|
|
191
|
+
</span>
|
|
192
|
+
<span class="text-subtitle-1 font-weight-bold">
|
|
193
|
+
${{ customTotalPrice.toLocaleString() }}
|
|
194
|
+
</span>
|
|
195
|
+
</v-sheet>
|
|
196
|
+
|
|
197
|
+
<p class="text-caption text-grey">
|
|
198
|
+
Calculated automatically from the selected applications.
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Global Pricing Error Message -->
|
|
203
|
+
<v-alert
|
|
204
|
+
v-if="errors.pricing"
|
|
205
|
+
type="error"
|
|
206
|
+
variant="tonal"
|
|
207
|
+
density="compact"
|
|
208
|
+
class="mt-2 text-caption"
|
|
209
|
+
>
|
|
210
|
+
{{ errors.pricing }}
|
|
211
|
+
</v-alert>
|
|
212
|
+
</v-card-text>
|
|
213
|
+
|
|
214
|
+
<v-divider />
|
|
215
|
+
|
|
216
|
+
<!-- Footer Buttons -->
|
|
217
|
+
<v-card-actions class="pt-3 px-2">
|
|
218
|
+
<v-btn
|
|
219
|
+
variant="outlined"
|
|
220
|
+
color="grey-darken-1"
|
|
221
|
+
class="flex-1 text-none"
|
|
222
|
+
@click="$emit('cancel')"
|
|
223
|
+
>
|
|
224
|
+
Cancel
|
|
225
|
+
</v-btn>
|
|
226
|
+
|
|
227
|
+
<v-btn
|
|
228
|
+
type="submit"
|
|
229
|
+
color="grey-darken-4"
|
|
230
|
+
variant="flat"
|
|
231
|
+
class="flex-1 text-none"
|
|
232
|
+
>
|
|
233
|
+
{{ isEditing ? "Save Changes" : "Create Plan" }}
|
|
234
|
+
</v-btn>
|
|
235
|
+
</v-card-actions>
|
|
236
|
+
</v-form>
|
|
237
|
+
</v-card>
|
|
238
|
+
</template>
|
|
239
|
+
|
|
240
|
+
<script setup lang="ts">
|
|
241
|
+
import { reactive, computed, watch } from "vue";
|
|
242
|
+
|
|
243
|
+
import {
|
|
244
|
+
AVAILABLE_APPLICATIONS,
|
|
245
|
+
type PlanFormState,
|
|
246
|
+
type SubscriptionPlan,
|
|
247
|
+
} from "../types/subscription";
|
|
248
|
+
|
|
249
|
+
const props = defineProps<{
|
|
250
|
+
initialPlan?: SubscriptionPlan | null;
|
|
251
|
+
}>();
|
|
252
|
+
|
|
253
|
+
const emit = defineEmits<{
|
|
254
|
+
(e: "cancel"): void;
|
|
255
|
+
(e: "submit", payload: PlanFormState): void;
|
|
256
|
+
}>();
|
|
257
|
+
|
|
258
|
+
// Select options
|
|
259
|
+
const planTypeOptions = [
|
|
260
|
+
{ title: "Free (Bundle)", value: "free_bundle" },
|
|
261
|
+
{ title: "Paid (Bundle)", value: "paid_bundle" },
|
|
262
|
+
{ title: "Paid (Custom Apps)", value: "custom_apps" },
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const billingCycleOptions = [
|
|
266
|
+
{ title: "Monthly", value: "monthly" },
|
|
267
|
+
{ title: "Annually", value: "annually" },
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const isEditing = computed(() => !!props.initialPlan);
|
|
271
|
+
|
|
272
|
+
const form = reactive<PlanFormState>({
|
|
273
|
+
name: props.initialPlan?.name ?? "",
|
|
274
|
+
description: props.initialPlan?.description ?? "",
|
|
275
|
+
maxSeats: props.initialPlan?.maxSeats ?? 0,
|
|
276
|
+
planType: props.initialPlan?.planType ?? "free_bundle",
|
|
277
|
+
billingCycle: props.initialPlan?.billingCycle ?? null,
|
|
278
|
+
bundleApps:
|
|
279
|
+
props.initialPlan?.planType !== "custom_apps"
|
|
280
|
+
? [...(props.initialPlan?.applicationIds ?? [])]
|
|
281
|
+
: [],
|
|
282
|
+
bundlePrice: props.initialPlan?.price ?? null,
|
|
283
|
+
customApps:
|
|
284
|
+
props.initialPlan?.planType === "custom_apps"
|
|
285
|
+
? [...(props.initialPlan?.applicationIds ?? [])]
|
|
286
|
+
: [],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const errors = reactive({
|
|
290
|
+
name: "",
|
|
291
|
+
pricing: "",
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/*
|
|
295
|
+
* Reset pricing fields when plan type changes
|
|
296
|
+
*/
|
|
297
|
+
watch(
|
|
298
|
+
() => form.planType,
|
|
299
|
+
(type) => {
|
|
300
|
+
if (type === "free_bundle") {
|
|
301
|
+
form.billingCycle = null;
|
|
302
|
+
form.bundlePrice = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
/*
|
|
308
|
+
* Toggle application selection helper
|
|
309
|
+
*/
|
|
310
|
+
function toggleApp(list: string[], id: string) {
|
|
311
|
+
const index = list.indexOf(id);
|
|
312
|
+
if (index === -1) {
|
|
313
|
+
list.push(id);
|
|
314
|
+
} else {
|
|
315
|
+
list.splice(index, 1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* Custom app total price calculation
|
|
321
|
+
*/
|
|
322
|
+
const customTotalPrice = computed(() => {
|
|
323
|
+
return form.customApps.reduce((total, id) => {
|
|
324
|
+
const app = AVAILABLE_APPLICATIONS.find((item) => item.id === id);
|
|
325
|
+
if (!app) return total;
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
total +
|
|
329
|
+
(form.billingCycle === "annually" ? app.yearlyPrice : app.monthlyPrice)
|
|
330
|
+
);
|
|
331
|
+
}, 0);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
/*
|
|
335
|
+
* Validate Form
|
|
336
|
+
*/
|
|
337
|
+
function validate(): boolean {
|
|
338
|
+
errors.name = form.name.trim() ? "" : "Plan name is required.";
|
|
339
|
+
errors.pricing = "";
|
|
340
|
+
|
|
341
|
+
if (form.planType !== "free_bundle" && !form.billingCycle) {
|
|
342
|
+
errors.pricing = "Please select a billing cycle.";
|
|
343
|
+
} else if (
|
|
344
|
+
form.planType === "paid_bundle" &&
|
|
345
|
+
(!form.bundlePrice || form.bundlePrice <= 0)
|
|
346
|
+
) {
|
|
347
|
+
errors.pricing = "Please enter a price.";
|
|
348
|
+
} else if (
|
|
349
|
+
form.planType === "custom_apps" &&
|
|
350
|
+
form.customApps.length === 0
|
|
351
|
+
) {
|
|
352
|
+
errors.pricing = "Select at least one application.";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return !errors.name && !errors.pricing;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/*
|
|
359
|
+
* Submit Form
|
|
360
|
+
*/
|
|
361
|
+
function submit() {
|
|
362
|
+
if (!validate()) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
emit("submit", {
|
|
367
|
+
...form,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
</script>
|
|
371
|
+
|
|
372
|
+
<style scoped>
|
|
373
|
+
.max-h-70vh {
|
|
374
|
+
max-height: 70vh;
|
|
375
|
+
}
|
|
376
|
+
</style>
|
|
@@ -1551,12 +1551,13 @@ const moduleWidgetPermissions: Record<string, string[]> = {
|
|
|
1551
1551
|
openWorkOrder: ["work_orders:see-all-work-orders"],
|
|
1552
1552
|
workOrders: ["work_orders:see-all-work-orders"],
|
|
1553
1553
|
activity: ["work_orders:see-all-work-orders"],
|
|
1554
|
-
|
|
1555
|
-
"
|
|
1556
|
-
"
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
"
|
|
1554
|
+
taskSchedule: [
|
|
1555
|
+
"cleaning-schedule-mgmt:see-all-schedules",
|
|
1556
|
+
"schedule-task-mgmt:see-all-schedule-tasks",
|
|
1557
|
+
],
|
|
1558
|
+
taskCompleted: [
|
|
1559
|
+
"cleaning-schedule-mgmt:see-all-schedules",
|
|
1560
|
+
"schedule-task-mgmt:see-all-schedule-tasks",
|
|
1560
1561
|
],
|
|
1561
1562
|
staffStatus: ["attendance-mgmt:see-all-attendance"],
|
|
1562
1563
|
attendance: ["attendance-mgmt:see-all-attendance"],
|