@7365admin1/layer-common 3.1.4-staging.53 → 3.2.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.
@@ -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>