@7365admin1/layer-common 3.0.14 → 3.0.16
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/ClientDetailForm.vue +831 -0
- package/components/ClientMain.vue +273 -206
- package/components/DashboardMain.vue +981 -38
- package/components/OnlineFormFill.vue +51 -1
- package/composables/useOrg.ts +17 -1
- package/composables/usePDFDownload.ts +23 -3
- package/package.json +1 -1
|
@@ -1,212 +1,243 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="page-wrap">
|
|
3
3
|
|
|
4
|
-
<!-- =====
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<div class="topbar
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<!-- Filters: chỉ hiện ở active/suspended -->
|
|
25
|
-
<div v-if="tab !== 'pending'" class="filter-wrap">
|
|
26
|
-
<div class="filter-field">
|
|
27
|
-
<label>Filter by status</label>
|
|
28
|
-
<select v-model="filterStatus" @change="handleFilterChange">
|
|
29
|
-
<option value="active">Active</option>
|
|
30
|
-
<option value="suspended">Suspended</option>
|
|
31
|
-
</select>
|
|
32
|
-
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
33
|
-
<path d="M1 1l4 4 4-4" stroke="#6B7280" stroke-width="1.4" stroke-linecap="round"/>
|
|
34
|
-
</svg>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<div class="filter-field">
|
|
38
|
-
<label>Filter by plan type</label>
|
|
39
|
-
<select v-model="filterPlan" @change="handleFilterChange">
|
|
40
|
-
<option value="">All</option>
|
|
41
|
-
<option v-for="app in APP_LIST" :key="app.value" :value="app.value">
|
|
42
|
-
{{ app.title }}
|
|
43
|
-
</option>
|
|
44
|
-
</select>
|
|
45
|
-
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
46
|
-
<path d="M1 1l4 4 4-4" stroke="#6B7280" stroke-width="1.4" stroke-linecap="round"/>
|
|
4
|
+
<!-- ===== TABLE VIEW: topbar + table đi chung 1 khối ===== -->
|
|
5
|
+
<template v-if="viewMode === 'table'">
|
|
6
|
+
|
|
7
|
+
<!-- ===== TOP BAR ===== -->
|
|
8
|
+
<div class="topbar">
|
|
9
|
+
<button class="btn-invite" @click="openInviteDialog">Invite Client</button>
|
|
10
|
+
|
|
11
|
+
<div class="topbar-right">
|
|
12
|
+
<div class="search-wrap">
|
|
13
|
+
<input
|
|
14
|
+
v-model="search"
|
|
15
|
+
type="text"
|
|
16
|
+
class="search-input"
|
|
17
|
+
placeholder="Search Client"
|
|
18
|
+
@input="handleSearch"
|
|
19
|
+
/>
|
|
20
|
+
<button v-if="search" class="clear-btn" @click="search = ''; handleSearch()">×</button>
|
|
21
|
+
<svg class="search-icon" viewBox="0 0 20 20" fill="none">
|
|
22
|
+
<circle cx="9" cy="9" r="6" stroke="#9CA3AF" stroke-width="1.5"/>
|
|
23
|
+
<path d="M13.5 13.5L17 17" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round"/>
|
|
47
24
|
</svg>
|
|
48
25
|
</div>
|
|
49
26
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
27
|
+
<!-- Filters: chỉ hiện ở active/suspended -->
|
|
28
|
+
<div v-if="tab !== 'pending'" class="filter-wrap">
|
|
29
|
+
<div class="filter-field">
|
|
30
|
+
<label>Filter by status</label>
|
|
31
|
+
<select v-model="filterStatus" @change="handleFilterChange">
|
|
32
|
+
<option value="active">Active</option>
|
|
33
|
+
<option value="suspended">Suspended</option>
|
|
34
|
+
</select>
|
|
35
|
+
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
36
|
+
<path d="M1 1l4 4 4-4" stroke="#6B7280" stroke-width="1.4" stroke-linecap="round"/>
|
|
37
|
+
</svg>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="filter-field">
|
|
41
|
+
<label>Filter by plan type</label>
|
|
42
|
+
<select v-model="filterPlan" @change="handleFilterChange">
|
|
43
|
+
<option value="">All</option>
|
|
44
|
+
<option v-for="app in APP_LIST" :key="app.value" :value="app.value">
|
|
45
|
+
{{ app.title }}
|
|
46
|
+
</option>
|
|
47
|
+
</select>
|
|
48
|
+
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
49
|
+
<path d="M1 1l4 4 4-4" stroke="#6B7280" stroke-width="1.4" stroke-linecap="round"/>
|
|
50
|
+
</svg>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="filter-field">
|
|
54
|
+
<label>Filter by billing cycle</label>
|
|
55
|
+
<select v-model="filterBilling" @change="handleFilterChange">
|
|
56
|
+
<option value="">All</option>
|
|
57
|
+
<option value="monthly">Monthly</option>
|
|
58
|
+
<option value="yearly">Yearly</option>
|
|
59
|
+
</select>
|
|
60
|
+
<svg class="chevron" viewBox="0 0 10 6" fill="none">
|
|
61
|
+
<path d="M1 1l4 4 4-4" stroke="#6B7280" stroke-width="1.4" stroke-linecap="round"/>
|
|
62
|
+
</svg>
|
|
63
|
+
</div>
|
|
60
64
|
</div>
|
|
61
65
|
</div>
|
|
62
66
|
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<!-- ===== TABLE CARD ===== -->
|
|
66
|
-
<div class="table-card">
|
|
67
67
|
|
|
68
|
-
<!--
|
|
69
|
-
<div class="
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
<!-- ===== TABLE CARD ===== -->
|
|
69
|
+
<div class="table-card">
|
|
70
|
+
|
|
71
|
+
<!-- Toolbar row -->
|
|
72
|
+
<div class="toolbar">
|
|
73
|
+
<div class="toolbar-left">
|
|
74
|
+
<button class="refresh-btn" :class="{ spinning: loading }" @click="handleRefresh">
|
|
75
|
+
<svg viewBox="0 0 18 18" fill="none" width="15" height="15">
|
|
76
|
+
<path d="M15.5 9A6.5 6.5 0 1 1 9 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
77
|
+
<path d="M9 0.5v3.5l3-1.75L9 0.5z" fill="currentColor"/>
|
|
78
|
+
</svg>
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="toolbar-right">
|
|
82
|
+
<span class="page-range-text">{{ pageRange }}</span>
|
|
83
|
+
<button class="page-btn" :disabled="page <= 1" @click="handleUpdatePage(page - 1)">
|
|
84
|
+
<svg viewBox="0 0 7 11" fill="none" width="7" height="11">
|
|
85
|
+
<path d="M6 1L1.5 5.5 6 10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
<button class="page-btn" :disabled="page >= pages" @click="handleUpdatePage(page + 1)">
|
|
89
|
+
<svg viewBox="0 0 7 11" fill="none" width="7" height="11">
|
|
90
|
+
<path d="M1 1l4.5 4.5L1 10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
91
|
+
</svg>
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
90
94
|
</div>
|
|
91
|
-
</div>
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
<!-- Tabs -->
|
|
97
|
+
<div class="tabs-row">
|
|
98
|
+
<button class="tab" :class="{ active: tab === 'active' }" @click="onTabChange('active')">Active</button>
|
|
99
|
+
<button class="tab" :class="{ active: tab === 'suspended' }" @click="onTabChange('suspended')">Suspended</button>
|
|
100
|
+
<button class="tab" :class="{ active: tab === 'pending' }" @click="onTabChange('pending')">Pending Invitation</button>
|
|
101
|
+
</div>
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
</button>
|
|
159
|
-
<div v-if="openMenuId === item._id" class="dropdown">
|
|
160
|
-
<button class="dd-item" @click="closeMenu">Edit</button>
|
|
161
|
-
<button class="dd-item danger" @click="closeMenu">
|
|
162
|
-
{{ item.status === 'suspended' ? 'Reactivate' : 'Suspend' }}
|
|
103
|
+
<!-- Loading bar -->
|
|
104
|
+
<div v-if="loading" class="loading-bar"><div class="loading-bar-fill" /></div>
|
|
105
|
+
|
|
106
|
+
<!-- TABLE: active / suspended -->
|
|
107
|
+
<div v-if="tab !== 'pending'" class="table-scroll">
|
|
108
|
+
<table>
|
|
109
|
+
<thead>
|
|
110
|
+
<tr>
|
|
111
|
+
<th>Organization Name</th>
|
|
112
|
+
<th>Email</th>
|
|
113
|
+
<th>Sites</th>
|
|
114
|
+
<th>Plan Type</th>
|
|
115
|
+
<th>Billing Cycle</th>
|
|
116
|
+
<th>Subscription Start</th>
|
|
117
|
+
<th>Subscription End</th>
|
|
118
|
+
<th>Status</th>
|
|
119
|
+
<th>Actions</th>
|
|
120
|
+
</tr>
|
|
121
|
+
</thead>
|
|
122
|
+
<tbody>
|
|
123
|
+
<tr v-if="!loading && items.length === 0">
|
|
124
|
+
<td colspan="9" class="empty">No records found.</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr v-for="item in items" :key="item._id">
|
|
127
|
+
<td class="td-name">{{ item.name }}</td>
|
|
128
|
+
<td class="td-muted">{{ item.email }}</td>
|
|
129
|
+
<td>
|
|
130
|
+
<span class="sites-badge">{{ item.sites }}</span>
|
|
131
|
+
</td>
|
|
132
|
+
<td>{{ item.planType }}</td>
|
|
133
|
+
<td class="td-muted">{{ item.billingCycle }}</td>
|
|
134
|
+
<td>
|
|
135
|
+
<span class="date-cell">
|
|
136
|
+
<svg viewBox="0 0 16 16" fill="none" width="13" height="13">
|
|
137
|
+
<rect x="1" y="3" width="14" height="12" rx="2" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
138
|
+
<path d="M1 7h14" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
139
|
+
<path d="M5 1v3M11 1v3" stroke="#9CA3AF" stroke-width="1.3" stroke-linecap="round"/>
|
|
140
|
+
</svg>
|
|
141
|
+
{{ item.subscriptionStart }}
|
|
142
|
+
</span>
|
|
143
|
+
</td>
|
|
144
|
+
<td>
|
|
145
|
+
<span class="date-cell">
|
|
146
|
+
<svg viewBox="0 0 16 16" fill="none" width="13" height="13">
|
|
147
|
+
<rect x="1" y="3" width="14" height="12" rx="2" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
148
|
+
<path d="M1 7h14" stroke="#9CA3AF" stroke-width="1.3"/>
|
|
149
|
+
<path d="M5 1v3M11 1v3" stroke="#9CA3AF" stroke-width="1.3" stroke-linecap="round"/>
|
|
150
|
+
</svg>
|
|
151
|
+
{{ item.subscriptionEnd }}
|
|
152
|
+
</span>
|
|
153
|
+
</td>
|
|
154
|
+
<td>
|
|
155
|
+
<span :class="['status-chip', item.status]">{{ item.status }}</span>
|
|
156
|
+
</td>
|
|
157
|
+
<td>
|
|
158
|
+
<div class="action-wrap">
|
|
159
|
+
<button class="dots-btn" @click.stop="toggleMenu(item._id)">
|
|
160
|
+
<span /><span /><span />
|
|
163
161
|
</button>
|
|
162
|
+
<div v-if="openMenuId === item._id" class="dropdown">
|
|
163
|
+
<button
|
|
164
|
+
class="dd-item"
|
|
165
|
+
@click="viewClient(item)"
|
|
166
|
+
>
|
|
167
|
+
View Organization
|
|
168
|
+
</button>
|
|
169
|
+
<button
|
|
170
|
+
v-if="item.status === 'suspended'"
|
|
171
|
+
class="dd-item"
|
|
172
|
+
@click="activateClient(item)"
|
|
173
|
+
>
|
|
174
|
+
Activate
|
|
175
|
+
</button>
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
v-else
|
|
179
|
+
class="dd-item danger"
|
|
180
|
+
@click="suspendClient(item)"
|
|
181
|
+
>
|
|
182
|
+
Suspend
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
164
185
|
</div>
|
|
165
|
-
</
|
|
166
|
-
</
|
|
167
|
-
</
|
|
168
|
-
</
|
|
169
|
-
</
|
|
170
|
-
</div>
|
|
186
|
+
</td>
|
|
187
|
+
</tr>
|
|
188
|
+
</tbody>
|
|
189
|
+
</table>
|
|
190
|
+
</div>
|
|
171
191
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
192
|
+
<!-- TABLE: pending -->
|
|
193
|
+
<div v-else class="table-scroll">
|
|
194
|
+
<table>
|
|
195
|
+
<thead>
|
|
196
|
+
<tr>
|
|
197
|
+
<th>Email</th>
|
|
198
|
+
<th>App</th>
|
|
199
|
+
<th>Role</th>
|
|
200
|
+
<th>Status</th>
|
|
201
|
+
<th>Action</th>
|
|
202
|
+
</tr>
|
|
203
|
+
</thead>
|
|
204
|
+
<tbody>
|
|
205
|
+
<tr v-if="!loading && items.length === 0">
|
|
206
|
+
<td colspan="5" class="empty">No pending invitations.</td>
|
|
207
|
+
</tr>
|
|
208
|
+
<tr v-for="item in items" :key="item._id">
|
|
209
|
+
<td class="td-muted">{{ item.email }}</td>
|
|
210
|
+
<td>{{ item.app }}</td>
|
|
211
|
+
<td class="td-muted">{{ item.role }}</td>
|
|
212
|
+
<td><span class="status-chip pending">{{ item.status }}</span></td>
|
|
213
|
+
<td>
|
|
214
|
+
<button
|
|
215
|
+
class="cancel-btn"
|
|
216
|
+
:disabled="cancelLoadingId === item._id"
|
|
217
|
+
@click="cancelInvite(item)"
|
|
218
|
+
>
|
|
219
|
+
<span v-if="cancelLoadingId === item._id" class="spinner" />
|
|
220
|
+
<span v-else>Cancel Invite</span>
|
|
221
|
+
</button>
|
|
222
|
+
</td>
|
|
223
|
+
</tr>
|
|
224
|
+
</tbody>
|
|
225
|
+
</table>
|
|
226
|
+
</div>
|
|
206
227
|
</div>
|
|
228
|
+
|
|
229
|
+
</template>
|
|
230
|
+
|
|
231
|
+
<!-- ===== DETAIL VIEW: chiếm toàn bộ page-wrap, không còn topbar/table ===== -->
|
|
232
|
+
<div v-else-if="viewMode === 'detail'" class="client-detail">
|
|
233
|
+
<ClientDetailForm
|
|
234
|
+
:client="selectedClient"
|
|
235
|
+
@cancel="cancelView"
|
|
236
|
+
@submit="submitClient"
|
|
237
|
+
/>
|
|
207
238
|
</div>
|
|
208
239
|
|
|
209
|
-
<!-- ===== DIALOG ===== -->
|
|
240
|
+
<!-- ===== DIALOG (luôn nằm ngoài, có thể mở ở bất kỳ view nào) ===== -->
|
|
210
241
|
<div v-if="dialog" class="overlay" @click.self="dialog = false">
|
|
211
242
|
<div class="dialog-box">
|
|
212
243
|
<InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
|
|
@@ -221,14 +252,16 @@ import useOrg from '../composables/useOrg'
|
|
|
221
252
|
import useVerification from '../composables/useVerification'
|
|
222
253
|
import useRole from '../composables/useRole'
|
|
223
254
|
import InvitationClientForm from './InvitationClientForm.vue'
|
|
255
|
+
import ClientDetailForm from './ClientDetailForm.vue'
|
|
224
256
|
import useCustomerSite from '../composables/useCustomerSite'
|
|
225
257
|
import useSubscription from '../composables/useSubscription'
|
|
226
258
|
|
|
259
|
+
|
|
227
260
|
/* ── TYPES ── */
|
|
228
261
|
type TabStatus = 'active' | 'suspended' | 'pending'
|
|
229
262
|
|
|
230
263
|
/* ── COMPOSABLES ── */
|
|
231
|
-
const { getOrganizationsWithSubscription } = useOrg()
|
|
264
|
+
const { getOrganizationsWithSubscription, updateStatus } = useOrg()
|
|
232
265
|
const { getVerifications, cancelUserInvitation } = useVerification()
|
|
233
266
|
const { getCustomerSites: getCustomerSitesByOrgId } = useCustomerSite()
|
|
234
267
|
const { getByOrgId: getSubscriptionByOrgId } = useSubscription()
|
|
@@ -264,6 +297,8 @@ const totalItems = ref(0)
|
|
|
264
297
|
const cancelLoadingId = ref<string | null>(null)
|
|
265
298
|
const limit = ref(10)
|
|
266
299
|
const roleCache = ref<Record<string, string>>({})
|
|
300
|
+
const viewMode = ref<'table' | 'detail'>('table')
|
|
301
|
+
const selectedClient = ref<any>(null)
|
|
267
302
|
|
|
268
303
|
/* ── COMPUTED ── */
|
|
269
304
|
const pageRange = computed(() => {
|
|
@@ -303,6 +338,25 @@ function parseTotalItems(pageRangeStr: string, fallback: number): number {
|
|
|
303
338
|
return match ? Number(match[1]) : fallback
|
|
304
339
|
}
|
|
305
340
|
|
|
341
|
+
async function suspendClient(item: any) {
|
|
342
|
+
try {
|
|
343
|
+
await updateStatus(item._id, "suspended")
|
|
344
|
+
closeMenu()
|
|
345
|
+
await fetchData()
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.error(err)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function activateClient(item: any) {
|
|
352
|
+
try {
|
|
353
|
+
await updateStatus(item._id, "active")
|
|
354
|
+
closeMenu()
|
|
355
|
+
await fetchData()
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error(err)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
306
360
|
|
|
307
361
|
async function fetchOrgs(status: 'active' | 'suspended') {
|
|
308
362
|
loading.value = true
|
|
@@ -339,24 +393,18 @@ async function fetchOrgs(status: 'active' | 'suspended') {
|
|
|
339
393
|
}
|
|
340
394
|
|
|
341
395
|
return {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
name: org.name,
|
|
345
|
-
email: org.email,
|
|
346
|
-
nature: org.nature,
|
|
347
|
-
|
|
348
|
-
sites: `${siteCount} Sites`,
|
|
396
|
+
...org,
|
|
349
397
|
|
|
350
|
-
|
|
398
|
+
planType: planTypeLabel(sub),
|
|
351
399
|
|
|
352
|
-
|
|
400
|
+
billingCycle: sub?.billingCycle ?? "-",
|
|
353
401
|
|
|
354
|
-
|
|
402
|
+
subscriptionStart: formatDate(sub?.createdAt),
|
|
355
403
|
|
|
356
|
-
|
|
404
|
+
subscriptionEnd: formatDate(sub?.nextBillingDate),
|
|
357
405
|
|
|
358
|
-
|
|
359
|
-
|
|
406
|
+
sites: `${siteCount} Sites`,
|
|
407
|
+
}
|
|
360
408
|
})
|
|
361
409
|
)
|
|
362
410
|
|
|
@@ -404,6 +452,23 @@ async function fetchData() {
|
|
|
404
452
|
}
|
|
405
453
|
}
|
|
406
454
|
|
|
455
|
+
function viewClient(item: any) {
|
|
456
|
+
selectedClient.value = item
|
|
457
|
+
viewMode.value = 'detail'
|
|
458
|
+
closeMenu()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function cancelView() {
|
|
462
|
+
selectedClient.value = null
|
|
463
|
+
viewMode.value = 'table'
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function submitClient() {
|
|
467
|
+
// await update API
|
|
468
|
+
|
|
469
|
+
viewMode.value = 'table'
|
|
470
|
+
fetchData()
|
|
471
|
+
}
|
|
407
472
|
/* ── WATCH: filterStatus ── */
|
|
408
473
|
watch(filterStatus, (val) => {
|
|
409
474
|
tab.value = val
|
|
@@ -785,7 +850,6 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
|
|
|
785
850
|
.dd-item.danger { color: #DC2626; }
|
|
786
851
|
.dd-item.danger:hover { background: #FEF2F2; }
|
|
787
852
|
|
|
788
|
-
/* ===== CANCEL INVITE ===== */
|
|
789
853
|
.cancel-btn {
|
|
790
854
|
display: inline-flex;
|
|
791
855
|
align-items: center;
|
|
@@ -811,7 +875,10 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
|
|
|
811
875
|
animation: spin 0.7s linear infinite;
|
|
812
876
|
}
|
|
813
877
|
|
|
814
|
-
|
|
878
|
+
.client-detail {
|
|
879
|
+
margin: 12px 0 0 0;
|
|
880
|
+
}
|
|
881
|
+
|
|
815
882
|
.overlay {
|
|
816
883
|
position: fixed;
|
|
817
884
|
inset: 0;
|