@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
|
@@ -247,6 +247,40 @@
|
|
|
247
247
|
{{ resolveText(block.text) }}
|
|
248
248
|
</p>
|
|
249
249
|
|
|
250
|
+
<!-- ORDERED LIST -->
|
|
251
|
+
<div v-else-if="block.type === 'ordered-list'" class="mb-3">
|
|
252
|
+
<div
|
|
253
|
+
v-if="block.title"
|
|
254
|
+
class="text-body-2 font-weight-bold mb-1"
|
|
255
|
+
style="text-decoration: underline; text-underline-offset: 3px"
|
|
256
|
+
>{{ block.title }}</div>
|
|
257
|
+
<ol style="padding-left: 22px; margin: 0">
|
|
258
|
+
<li
|
|
259
|
+
v-for="(item, itemIdx) in block.items"
|
|
260
|
+
:key="itemIdx"
|
|
261
|
+
class="text-body-2 mb-1"
|
|
262
|
+
style="line-height: 1.55; color: #333"
|
|
263
|
+
>{{ item }}</li>
|
|
264
|
+
</ol>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- UNORDERED LIST -->
|
|
268
|
+
<div v-else-if="block.type === 'unordered-list'" class="mb-3">
|
|
269
|
+
<div
|
|
270
|
+
v-if="block.title"
|
|
271
|
+
class="text-body-2 font-weight-bold mb-1"
|
|
272
|
+
style="text-decoration: underline; text-underline-offset: 3px"
|
|
273
|
+
>{{ block.title }}</div>
|
|
274
|
+
<ul style="padding-left: 22px; margin: 0">
|
|
275
|
+
<li
|
|
276
|
+
v-for="(item, itemIdx) in block.items"
|
|
277
|
+
:key="itemIdx"
|
|
278
|
+
class="text-body-2 mb-1"
|
|
279
|
+
style="line-height: 1.55; color: #333"
|
|
280
|
+
>{{ item }}</li>
|
|
281
|
+
</ul>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
250
284
|
<!-- DIVIDER -->
|
|
251
285
|
<v-divider v-else-if="block.type === 'divider'" class="my-4" />
|
|
252
286
|
|
|
@@ -433,6 +467,7 @@ const props = defineProps({
|
|
|
433
467
|
approvalMode: { type: Boolean, default: false },
|
|
434
468
|
initialManagementValues: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
|
|
435
469
|
formType: { type: String, default: "" },
|
|
470
|
+
paymentAmount: { type: Number, default: 0 },
|
|
436
471
|
/** Optional custom submit handler. Receives the collected form values.
|
|
437
472
|
* If omitted the component submits to /api/online-forms automatically. */
|
|
438
473
|
submitFn: {
|
|
@@ -596,15 +631,8 @@ const submissionStatusColor = computed(() => {
|
|
|
596
631
|
}
|
|
597
632
|
});
|
|
598
633
|
|
|
599
|
-
const PAYMENT_REQUIRED_FORM_TYPES = new Set([
|
|
600
|
-
"Application for Electronic Access Card",
|
|
601
|
-
"Application for Booking of BBQ Pit",
|
|
602
|
-
"Application for Moving In/Out & Delivery",
|
|
603
|
-
"Application for Main Entrance RFID Transponder",
|
|
604
|
-
]);
|
|
605
|
-
|
|
606
634
|
const requiresPayment = computed(() => {
|
|
607
|
-
if (
|
|
635
|
+
if (props.paymentAmount <= 0) return false;
|
|
608
636
|
if (props.formType === "Application for Main Entrance RFID Transponder") {
|
|
609
637
|
return !!(props.initialValues.reasonLost || props.initialValues.reasonDamaged);
|
|
610
638
|
}
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
>
|
|
89
89
|
|
|
90
90
|
<!-- Create Dialog -->
|
|
91
|
-
<v-dialog v-model="createDialog" width="
|
|
91
|
+
<v-dialog v-model="createDialog" width="720" persistent>
|
|
92
92
|
<OnlineFormConfigurationForm
|
|
93
93
|
mode="add"
|
|
94
94
|
@cancel="createDialog = false"
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
</v-dialog>
|
|
98
98
|
|
|
99
99
|
<!-- Edit Dialog -->
|
|
100
|
-
<v-dialog v-model="editDialog" width="
|
|
100
|
+
<v-dialog v-model="editDialog" width="720" persistent>
|
|
101
101
|
<OnlineFormConfigurationForm
|
|
102
102
|
mode="edit"
|
|
103
103
|
@cancel="editDialog = false"
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
:definition="selectedFormDefinition"
|
|
114
114
|
:site-id="siteId"
|
|
115
115
|
:org-id="orgId"
|
|
116
|
+
:payment-amount="selectedPaymentAmount"
|
|
116
117
|
readonly
|
|
117
118
|
@cancel="previewDialog = false"
|
|
118
119
|
>
|
|
@@ -336,6 +337,9 @@ const selectedFormDefinition = ref<any>(null);
|
|
|
336
337
|
const deleteLoading = ref(false);
|
|
337
338
|
const confirmDialog = ref(false);
|
|
338
339
|
const selectedOnlineFormId = ref<string | null>(null);
|
|
340
|
+
const selectedPaymentAmount = computed(() =>
|
|
341
|
+
Number(selectedOnlineForm.value?.fields?.paymentAmount ?? 0)
|
|
342
|
+
);
|
|
339
343
|
|
|
340
344
|
function parseFieldsToInputs(fields: Record<string, string>): any[] {
|
|
341
345
|
return Object.keys(fields)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-menu location="bottom end">
|
|
3
|
+
<template #activator="{ props }">
|
|
4
|
+
<v-btn
|
|
5
|
+
v-bind="props"
|
|
6
|
+
icon="mdi-dots-vertical"
|
|
7
|
+
variant="text"
|
|
8
|
+
density="comfortable"
|
|
9
|
+
/>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<v-list density="compact">
|
|
13
|
+
<v-list-item
|
|
14
|
+
prepend-icon="mdi-pencil-outline"
|
|
15
|
+
title="Edit plan"
|
|
16
|
+
@click="$emit('edit')"
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
<v-list-item
|
|
20
|
+
prepend-icon="mdi-close-circle-outline"
|
|
21
|
+
title="Deactivate"
|
|
22
|
+
@click="$emit('deactivate')"
|
|
23
|
+
/>
|
|
24
|
+
</v-list>
|
|
25
|
+
</v-menu>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
defineEmits<{
|
|
30
|
+
edit: []
|
|
31
|
+
deactivate: []
|
|
32
|
+
}>()
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-chip
|
|
3
|
+
:color="isActive ? 'success' : 'grey'"
|
|
4
|
+
size="small"
|
|
5
|
+
variant="tonal"
|
|
6
|
+
class="font-weight-medium"
|
|
7
|
+
>
|
|
8
|
+
{{ isActive ? 'Active' : 'Deactive' }}
|
|
9
|
+
</v-chip>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { computed } from 'vue'
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
status: string | boolean
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const isActive = computed(() => {
|
|
20
|
+
if (typeof props.status === 'boolean') {
|
|
21
|
+
return props.status
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return props.status.toLowerCase() === 'active'
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container fluid class="pa-6">
|
|
3
|
+
<!-- Header: Title & Create Button -->
|
|
4
|
+
<div class="d-flex align-center justify-space-between mb-6">
|
|
5
|
+
<h1 class="text-h5 font-weight-bold text-primary">
|
|
6
|
+
Subscription Plans
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+
<v-btn
|
|
10
|
+
color="secondary"
|
|
11
|
+
rounded="pill"
|
|
12
|
+
elevation="0"
|
|
13
|
+
@click="openCreateModal"
|
|
14
|
+
>
|
|
15
|
+
Create Plan
|
|
16
|
+
</v-btn>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Search & Filters Bar -->
|
|
20
|
+
<v-row class="mb-2" density="compact">
|
|
21
|
+
<!-- Search Input -->
|
|
22
|
+
<v-col cols="12" sm="4" md="3">
|
|
23
|
+
<v-text-field
|
|
24
|
+
v-model="search"
|
|
25
|
+
placeholder="Search Plan"
|
|
26
|
+
prepend-inner-icon="mdi-magnify"
|
|
27
|
+
variant="outlined"
|
|
28
|
+
density="compact"
|
|
29
|
+
hide-details
|
|
30
|
+
clearable
|
|
31
|
+
/>
|
|
32
|
+
</v-col>
|
|
33
|
+
|
|
34
|
+
<!-- Filter by Category -->
|
|
35
|
+
<v-col cols="12" sm="4" md="3">
|
|
36
|
+
<v-select
|
|
37
|
+
v-model="categoryFilter"
|
|
38
|
+
:items="categoryOptions"
|
|
39
|
+
label="Filter by category"
|
|
40
|
+
variant="outlined"
|
|
41
|
+
density="compact"
|
|
42
|
+
hide-details
|
|
43
|
+
/>
|
|
44
|
+
</v-col>
|
|
45
|
+
|
|
46
|
+
<!-- Filter by Billing Cycle -->
|
|
47
|
+
<v-col cols="12" sm="4" md="3">
|
|
48
|
+
<v-select
|
|
49
|
+
v-model="billingFilter"
|
|
50
|
+
:items="billingOptions"
|
|
51
|
+
label="Filter by billing cycle"
|
|
52
|
+
variant="outlined"
|
|
53
|
+
density="compact"
|
|
54
|
+
hide-details
|
|
55
|
+
/>
|
|
56
|
+
</v-col>
|
|
57
|
+
</v-row>
|
|
58
|
+
|
|
59
|
+
<!-- Main Table Card -->
|
|
60
|
+
<v-card variant="outlined" class="mt-4">
|
|
61
|
+
<!-- Status Tabs & Actions Bar -->
|
|
62
|
+
<div class="d-flex align-center justify-space-between bg-grey-lighten-4 border-b">
|
|
63
|
+
<v-tabs v-model="activeTab" color="primary" density="compact">
|
|
64
|
+
<v-tab value="active" class="text-none font-weight-bold">Active</v-tab>
|
|
65
|
+
<v-tab value="deactive" class="text-none font-weight-bold">Deactive</v-tab>
|
|
66
|
+
</v-tabs>
|
|
67
|
+
|
|
68
|
+
<div class="pa-2">
|
|
69
|
+
<v-btn
|
|
70
|
+
icon="mdi-refresh"
|
|
71
|
+
variant="text"
|
|
72
|
+
density="comfortable"
|
|
73
|
+
title="Refresh"
|
|
74
|
+
@click="refresh"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Table View -->
|
|
80
|
+
<v-data-table
|
|
81
|
+
:headers="headers"
|
|
82
|
+
:items="filteredPlans"
|
|
83
|
+
:page="page"
|
|
84
|
+
:items-per-page="pageSize"
|
|
85
|
+
class="elevation-0"
|
|
86
|
+
>
|
|
87
|
+
<!-- Plan Name -->
|
|
88
|
+
<template #item.name="{ item }">
|
|
89
|
+
<span class="font-weight-bold text-primary">{{ item.name }}</span>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<!-- Plan Type -->
|
|
93
|
+
<template #item.planType="{ item }">
|
|
94
|
+
{{ item.planType === 'free_bundle' ? 'Free' : 'Paid' }}
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<!-- Billing Cycle -->
|
|
98
|
+
<template #item.billingCycle="{ item }">
|
|
99
|
+
{{ formatBillingCycle(item.billingCycle) }}
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<!-- Price -->
|
|
103
|
+
<template #item.price="{ item }">
|
|
104
|
+
<span class="font-weight-semibold text-primary">
|
|
105
|
+
{{ formatPrice(item) }}
|
|
106
|
+
</span>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<!-- Applications Badge -->
|
|
110
|
+
<template #item.applicationIds="{ item }">
|
|
111
|
+
<v-chip size="small" variant="tonal" color="grey-darken-2">
|
|
112
|
+
{{ item.applicationIds.length }} Application{{ item.applicationIds.length > 1 ? 's' : '' }}
|
|
113
|
+
</v-chip>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<!-- Status Badge -->
|
|
117
|
+
<template #item.status="{ item }">
|
|
118
|
+
<v-chip
|
|
119
|
+
size="small"
|
|
120
|
+
:color="item.status === 'active' ? 'success' : 'grey'"
|
|
121
|
+
variant="flat"
|
|
122
|
+
class="text-caption font-weight-bold"
|
|
123
|
+
>
|
|
124
|
+
{{ item.status === 'active' ? 'Active' : 'Deactive' }}
|
|
125
|
+
</v-chip>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<!-- Actions -->
|
|
129
|
+
<template #item.actions="{ item }">
|
|
130
|
+
<v-menu location="bottom end" transition="scale-transition">
|
|
131
|
+
<template #activator="{ props }">
|
|
132
|
+
<v-btn
|
|
133
|
+
v-bind="props"
|
|
134
|
+
icon="mdi-dots-horizontal"
|
|
135
|
+
variant="text"
|
|
136
|
+
density="comfortable"
|
|
137
|
+
color="grey-darken-1"
|
|
138
|
+
/>
|
|
139
|
+
</template>
|
|
140
|
+
|
|
141
|
+
<v-card min-width="160" class="py-1" elevation="3" rounded="lg">
|
|
142
|
+
<v-list density="compact" class="py-0">
|
|
143
|
+
<!-- Edit Plan -->
|
|
144
|
+
<v-list-item
|
|
145
|
+
prepend-icon="mdi-square-edit-outline"
|
|
146
|
+
title="Edit plan"
|
|
147
|
+
value="edit"
|
|
148
|
+
class="text-body-2"
|
|
149
|
+
@click="openEditModal(item)"
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
<v-divider class="my-1" />
|
|
153
|
+
|
|
154
|
+
<!-- Toggle Status (Deactivate / Activate) -->
|
|
155
|
+
<v-list-item
|
|
156
|
+
:prepend-icon="item.status === 'active' ? 'mdi-trash-can-outline' : 'mdi-check-circle-outline'"
|
|
157
|
+
:title="item.status === 'active' ? 'Deactivate' : 'Activate'"
|
|
158
|
+
:class="item.status === 'active' ? 'text-error' : 'text-success'"
|
|
159
|
+
value="toggle-status"
|
|
160
|
+
class="text-body-2"
|
|
161
|
+
@click="togglePlanStatus(item)"
|
|
162
|
+
/>
|
|
163
|
+
</v-list>
|
|
164
|
+
</v-card>
|
|
165
|
+
</v-menu>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
<!-- Empty State -->
|
|
169
|
+
<template #no-data>
|
|
170
|
+
<div class="py-8 text-center text-grey-darken-1">
|
|
171
|
+
No plans found.
|
|
172
|
+
</div>
|
|
173
|
+
</template>
|
|
174
|
+
</v-data-table>
|
|
175
|
+
</v-card>
|
|
176
|
+
|
|
177
|
+
<!-- Create / Edit Dialog -->
|
|
178
|
+
<v-dialog v-model="showModal" max-width="600px" persistent>
|
|
179
|
+
<v-card class="pa-4">
|
|
180
|
+
<CreateSubscriptionPlan
|
|
181
|
+
:initial-plan="editingPlan"
|
|
182
|
+
@cancel="closeModal"
|
|
183
|
+
@submit="handleSubmit"
|
|
184
|
+
/>
|
|
185
|
+
</v-card>
|
|
186
|
+
</v-dialog>
|
|
187
|
+
</v-container>
|
|
188
|
+
</template>
|
|
189
|
+
|
|
190
|
+
<script setup lang="ts">
|
|
191
|
+
import { ref, reactive, computed, watch } from "vue";
|
|
192
|
+
import CreateSubscriptionPlan from "./CreateSubsciptionPlan.vue";
|
|
193
|
+
|
|
194
|
+
import type {
|
|
195
|
+
SubscriptionPlan,
|
|
196
|
+
PlanFormState,
|
|
197
|
+
BillingCycle,
|
|
198
|
+
} from "../types/subscription";
|
|
199
|
+
|
|
200
|
+
// Datatable Headers
|
|
201
|
+
const headers = [
|
|
202
|
+
{ title: "Plan Name", key: "name", align: "start" as const },
|
|
203
|
+
{ title: "Plan Type", key: "planType", align: "start" as const },
|
|
204
|
+
{ title: "Billing Cycle", key: "billingCycle", align: "start" as const },
|
|
205
|
+
{ title: "Price", key: "price", align: "start" as const },
|
|
206
|
+
{ title: "Applications", key: "applicationIds", align: "center" as const },
|
|
207
|
+
{ title: "Status", key: "status", align: "center" as const },
|
|
208
|
+
{ title: "Actions", key: "actions", align: "center" as const, sortable: false },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
// Select Options
|
|
212
|
+
const categoryOptions = [
|
|
213
|
+
{ title: "All", value: "all" },
|
|
214
|
+
{ title: "Free (Bundle)", value: "free_bundle" },
|
|
215
|
+
{ title: "Paid (Bundle)", value: "paid_bundle" },
|
|
216
|
+
{ title: "Paid (Custom Apps)", value: "custom_apps" },
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const billingOptions = [
|
|
220
|
+
{ title: "All", value: "all" },
|
|
221
|
+
{ title: "Monthly", value: "monthly" },
|
|
222
|
+
{ title: "Yearly", value: "annually" },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const plans = reactive<SubscriptionPlan[]>([
|
|
226
|
+
{
|
|
227
|
+
id: "1",
|
|
228
|
+
name: "Standard",
|
|
229
|
+
description: "",
|
|
230
|
+
maxSeats: 10,
|
|
231
|
+
planType: "paid_bundle",
|
|
232
|
+
billingCycle: "monthly",
|
|
233
|
+
price: 299,
|
|
234
|
+
applicationIds: ["crm"],
|
|
235
|
+
status: "active",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "2",
|
|
239
|
+
name: "Premium",
|
|
240
|
+
description: "",
|
|
241
|
+
maxSeats: 25,
|
|
242
|
+
planType: "paid_bundle",
|
|
243
|
+
billingCycle: "annually",
|
|
244
|
+
price: 299,
|
|
245
|
+
applicationIds: ["crm", "hrm"],
|
|
246
|
+
status: "active",
|
|
247
|
+
},
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
const search = ref("");
|
|
251
|
+
const categoryFilter = ref("all");
|
|
252
|
+
const billingFilter = ref<"all" | BillingCycle>("all");
|
|
253
|
+
const activeTab = ref<"active" | "deactive">("active");
|
|
254
|
+
|
|
255
|
+
const page = ref(1);
|
|
256
|
+
const pageSize = 20;
|
|
257
|
+
|
|
258
|
+
const showModal = ref(false);
|
|
259
|
+
const editingPlan = ref<SubscriptionPlan | null>(null);
|
|
260
|
+
|
|
261
|
+
const filteredPlans = computed(() =>
|
|
262
|
+
plans.filter((plan) => {
|
|
263
|
+
if (plan.status !== activeTab.value) return false;
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
search.value &&
|
|
267
|
+
!plan.name.toLowerCase().includes(search.value.toLowerCase())
|
|
268
|
+
) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (categoryFilter.value !== "all" && plan.planType !== categoryFilter.value) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (billingFilter.value !== "all" && plan.billingCycle !== billingFilter.value) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return true;
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
watch([search, categoryFilter, billingFilter, activeTab], () => {
|
|
285
|
+
page.value = 1;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
function formatBillingCycle(cycle?: string) {
|
|
289
|
+
if (!cycle) return "—";
|
|
290
|
+
if (cycle === "annually" || cycle === "yearly") return "Yearly";
|
|
291
|
+
if (cycle === "monthly") return "Monthly";
|
|
292
|
+
return cycle;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function formatPrice(plan: SubscriptionPlan) {
|
|
296
|
+
if (plan.planType === "free_bundle") return "Free";
|
|
297
|
+
if (plan.price == null) return "—";
|
|
298
|
+
return `$${plan.price}/${plan.billingCycle === "annually" ? "yr" : "mo"}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function refresh() {
|
|
302
|
+
page.value = 1;
|
|
303
|
+
search.value = "";
|
|
304
|
+
categoryFilter.value = "all";
|
|
305
|
+
billingFilter.value = "all";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function openCreateModal() {
|
|
309
|
+
editingPlan.value = null;
|
|
310
|
+
showModal.value = true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function openEditModal(plan: SubscriptionPlan) {
|
|
314
|
+
editingPlan.value = plan;
|
|
315
|
+
showModal.value = true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function closeModal() {
|
|
319
|
+
showModal.value = false;
|
|
320
|
+
editingPlan.value = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function handleSubmit(payload: PlanFormState) {
|
|
324
|
+
const applicationIds =
|
|
325
|
+
payload.planType === "custom_apps"
|
|
326
|
+
? payload.customApps
|
|
327
|
+
: payload.bundleApps;
|
|
328
|
+
|
|
329
|
+
const price =
|
|
330
|
+
payload.planType === "custom_apps"
|
|
331
|
+
? null
|
|
332
|
+
: payload.planType === "paid_bundle"
|
|
333
|
+
? payload.bundlePrice
|
|
334
|
+
: null;
|
|
335
|
+
|
|
336
|
+
if (editingPlan.value) {
|
|
337
|
+
Object.assign(editingPlan.value, {
|
|
338
|
+
name: payload.name,
|
|
339
|
+
description: payload.description,
|
|
340
|
+
maxSeats: payload.maxSeats,
|
|
341
|
+
planType: payload.planType,
|
|
342
|
+
billingCycle: payload.billingCycle,
|
|
343
|
+
price,
|
|
344
|
+
applicationIds,
|
|
345
|
+
});
|
|
346
|
+
} else {
|
|
347
|
+
plans.push({
|
|
348
|
+
id: crypto.randomUUID(),
|
|
349
|
+
name: payload.name,
|
|
350
|
+
description: payload.description,
|
|
351
|
+
maxSeats: payload.maxSeats,
|
|
352
|
+
planType: payload.planType,
|
|
353
|
+
billingCycle: payload.billingCycle,
|
|
354
|
+
price,
|
|
355
|
+
applicationIds,
|
|
356
|
+
status: "active",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
closeModal();
|
|
361
|
+
}
|
|
362
|
+
</script>
|
|
@@ -20,6 +20,8 @@ export type FormBlock =
|
|
|
20
20
|
| { type: "checkbox-group"; title?: string; single?: boolean; items: CheckboxItemDef[] }
|
|
21
21
|
| { type: "paragraph"; text: string }
|
|
22
22
|
| { type: "section-header"; text: string }
|
|
23
|
+
| { type: "ordered-list"; title?: string; items: string[] }
|
|
24
|
+
| { type: "unordered-list"; title?: string; items: string[] }
|
|
23
25
|
| { type: "divider" }
|
|
24
26
|
| { type: "signature-row"; label: string; key: string }
|
|
25
27
|
| { type: "acknowledgement"; text: string }
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type PlanStatus = "active" | "deactive";
|
|
2
|
+
|
|
3
|
+
export type PlanType = "free_bundle" | "paid_bundle" | "custom_apps";
|
|
4
|
+
|
|
5
|
+
export type BillingCycle = "monthly" | "annually";
|
|
6
|
+
|
|
7
|
+
export interface AppOption {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
monthlyPrice: number;
|
|
11
|
+
yearlyPrice: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SubscriptionPlan {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
maxSeats: number;
|
|
19
|
+
planType: PlanType;
|
|
20
|
+
billingCycle: BillingCycle | null;
|
|
21
|
+
price: number | null;
|
|
22
|
+
applicationIds: string[];
|
|
23
|
+
status: PlanStatus;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PlanFormState {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
maxSeats: number;
|
|
30
|
+
planType: PlanType;
|
|
31
|
+
billingCycle: BillingCycle | null;
|
|
32
|
+
bundleApps: string[];
|
|
33
|
+
bundlePrice: number | null;
|
|
34
|
+
customApps: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const PLAN_TYPE_LABEL: Record<PlanType, string> = {
|
|
38
|
+
free_bundle: "Free (Bundle)",
|
|
39
|
+
paid_bundle: "Paid (Bundle)",
|
|
40
|
+
custom_apps: "Paid (Custom Apps)",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Mock catalogue of applications that can be bundled into a plan.
|
|
44
|
+
// Replace with data fetched from your backend.
|
|
45
|
+
export const AVAILABLE_APPLICATIONS: AppOption[] = [
|
|
46
|
+
{ id: "crm", name: "CRM", monthlyPrice: 49, yearlyPrice: 490 },
|
|
47
|
+
{ id: "hrm", name: "HRM", monthlyPrice: 39, yearlyPrice: 390 },
|
|
48
|
+
{ id: "accounting", name: "Accounting", monthlyPrice: 59, yearlyPrice: 590 },
|
|
49
|
+
{ id: "inventory", name: "Inventory", monthlyPrice: 45, yearlyPrice: 450 },
|
|
50
|
+
{ id: "pos", name: "POS", monthlyPrice: 69, yearlyPrice: 690 },
|
|
51
|
+
];
|