@eeplatform/nuxt-layer-common 1.2.9 → 1.2.11
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/MemberMain.vue +10 -8
- package/components/RolePermissionMain.vue +1 -1
- package/components/SchoolFormCreate.vue +10 -8
- package/components/SchoolFormUpload.vue +403 -0
- package/components/SchoolMain.vue +66 -22
- package/composables/useSchool.ts +34 -0
- package/package.json +1 -1
- package/types/school.d.ts +13 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @eeplatform/nuxt-layer-common
|
|
2
2
|
|
|
3
|
+
## 1.2.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11f490f: School Mgmt - table hight and props adjustment
|
|
8
|
+
|
|
9
|
+
## 1.2.10
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- c16019c: School Mgmt - Admin only, implement add and bulk add
|
|
14
|
+
|
|
3
15
|
## 1.2.9
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -323,14 +323,16 @@ const {
|
|
|
323
323
|
data: getAllReq,
|
|
324
324
|
refresh: getAll,
|
|
325
325
|
status: getAllReqStatus,
|
|
326
|
-
} = useLazyAsyncData(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
326
|
+
} = useLazyAsyncData(
|
|
327
|
+
"get-all-members-by-status" + props.status + "-" + props.orgId,
|
|
328
|
+
() =>
|
|
329
|
+
_getAll({
|
|
330
|
+
status: props.status,
|
|
331
|
+
org: props.orgId,
|
|
332
|
+
search: headerSearch.value,
|
|
333
|
+
page: page.value,
|
|
334
|
+
type: props.type,
|
|
335
|
+
})
|
|
334
336
|
);
|
|
335
337
|
|
|
336
338
|
const loading = computed(() => getAllReqStatus.value === "pending");
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
<v-card width="100%">
|
|
3
3
|
<v-toolbar>
|
|
4
4
|
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
|
-
<span class="font-weight-bold text-h5">
|
|
6
|
-
Create School
|
|
7
|
-
</span>
|
|
5
|
+
<span class="font-weight-bold text-h5"> Create School </span>
|
|
8
6
|
</v-row>
|
|
9
7
|
</v-toolbar>
|
|
10
8
|
<v-card-text style="max-height: 100vh; overflow-y: auto">
|
|
@@ -12,7 +10,11 @@
|
|
|
12
10
|
<v-row no-gutters>
|
|
13
11
|
<v-col cols="12" class="mt-2">
|
|
14
12
|
<v-row no-gutters>
|
|
15
|
-
<InputLabel
|
|
13
|
+
<InputLabel
|
|
14
|
+
class="text-capitalize"
|
|
15
|
+
title="School Name"
|
|
16
|
+
required
|
|
17
|
+
/>
|
|
16
18
|
<v-col cols="12">
|
|
17
19
|
<v-text-field
|
|
18
20
|
v-model="name"
|
|
@@ -145,7 +147,7 @@ const { requiredRule } = useUtils();
|
|
|
145
147
|
|
|
146
148
|
const message = ref("");
|
|
147
149
|
|
|
148
|
-
const {
|
|
150
|
+
const { addSchool } = useSchool();
|
|
149
151
|
const { getAll: getDivisions } = useDivision();
|
|
150
152
|
|
|
151
153
|
// Load divisions for selection
|
|
@@ -158,7 +160,7 @@ onMounted(async () => {
|
|
|
158
160
|
const response = await getDivisions({ page: 1, limit: 100 });
|
|
159
161
|
divisionOptions.value = response.items || [];
|
|
160
162
|
} catch (error) {
|
|
161
|
-
console.error(
|
|
163
|
+
console.error("Failed to load divisions:", error);
|
|
162
164
|
} finally {
|
|
163
165
|
divisionsLoading.value = false;
|
|
164
166
|
}
|
|
@@ -167,7 +169,7 @@ onMounted(async () => {
|
|
|
167
169
|
async function submit() {
|
|
168
170
|
disable.value = true;
|
|
169
171
|
try {
|
|
170
|
-
const payload:
|
|
172
|
+
const payload: TSchool = {
|
|
171
173
|
name: name.value,
|
|
172
174
|
};
|
|
173
175
|
|
|
@@ -184,7 +186,7 @@ async function submit() {
|
|
|
184
186
|
payload.address = address.value.trim();
|
|
185
187
|
}
|
|
186
188
|
|
|
187
|
-
await
|
|
189
|
+
await addSchool(payload);
|
|
188
190
|
|
|
189
191
|
if (createMore.value) {
|
|
190
192
|
name.value = "";
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
|
+
<span class="font-weight-bold text-h5"> Bulk Upload Schools </span>
|
|
6
|
+
</v-row>
|
|
7
|
+
</v-toolbar>
|
|
8
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto">
|
|
9
|
+
<v-form v-model="validForm" :disabled="disable">
|
|
10
|
+
<v-row no-gutters>
|
|
11
|
+
<v-col cols="12" class="mt-2">
|
|
12
|
+
<v-row no-gutters>
|
|
13
|
+
<InputLabel class="text-capitalize" title="Region" required />
|
|
14
|
+
<v-col cols="12">
|
|
15
|
+
<v-select
|
|
16
|
+
v-model="selectedRegion"
|
|
17
|
+
:items="regionOptions"
|
|
18
|
+
item-title="name"
|
|
19
|
+
item-value="_id"
|
|
20
|
+
density="comfortable"
|
|
21
|
+
:rules="[requiredRule]"
|
|
22
|
+
placeholder="Select a region"
|
|
23
|
+
:loading="regionsLoading"
|
|
24
|
+
return-object
|
|
25
|
+
></v-select>
|
|
26
|
+
</v-col>
|
|
27
|
+
</v-row>
|
|
28
|
+
</v-col>
|
|
29
|
+
|
|
30
|
+
<v-col cols="12" class="mt-2">
|
|
31
|
+
<v-row no-gutters>
|
|
32
|
+
<InputLabel class="text-capitalize" title="Division" required />
|
|
33
|
+
<v-col cols="12">
|
|
34
|
+
<v-select
|
|
35
|
+
v-model="selectedDivision"
|
|
36
|
+
:items="divisionOptions"
|
|
37
|
+
item-title="name"
|
|
38
|
+
item-value="_id"
|
|
39
|
+
density="comfortable"
|
|
40
|
+
:rules="[requiredRule]"
|
|
41
|
+
placeholder="Select a division"
|
|
42
|
+
:loading="divisionsLoading"
|
|
43
|
+
return-object
|
|
44
|
+
></v-select>
|
|
45
|
+
</v-col>
|
|
46
|
+
</v-row>
|
|
47
|
+
</v-col>
|
|
48
|
+
|
|
49
|
+
<v-col cols="12" class="mt-4">
|
|
50
|
+
<v-row no-gutters>
|
|
51
|
+
<v-col cols="12">
|
|
52
|
+
<v-btn
|
|
53
|
+
variant="outlined"
|
|
54
|
+
color="primary"
|
|
55
|
+
class="text-none"
|
|
56
|
+
size="large"
|
|
57
|
+
block
|
|
58
|
+
prepend-icon="mdi-download"
|
|
59
|
+
@click="downloadTemplate"
|
|
60
|
+
:disabled="disable"
|
|
61
|
+
>
|
|
62
|
+
Download Template
|
|
63
|
+
</v-btn>
|
|
64
|
+
</v-col>
|
|
65
|
+
</v-row>
|
|
66
|
+
</v-col>
|
|
67
|
+
|
|
68
|
+
<v-col cols="12" class="mt-2">
|
|
69
|
+
<v-divider></v-divider>
|
|
70
|
+
</v-col>
|
|
71
|
+
|
|
72
|
+
<v-file-input
|
|
73
|
+
ref="fileInputRef"
|
|
74
|
+
v-model="selectedFile"
|
|
75
|
+
v-show="false"
|
|
76
|
+
accept=".csv,.xlsx,.xls"
|
|
77
|
+
@change="handleFileSelect"
|
|
78
|
+
></v-file-input>
|
|
79
|
+
|
|
80
|
+
<v-col cols="12">
|
|
81
|
+
<v-btn
|
|
82
|
+
block
|
|
83
|
+
variant="flat"
|
|
84
|
+
color="black"
|
|
85
|
+
class="text-none"
|
|
86
|
+
size="large"
|
|
87
|
+
@click="triggerFileInput"
|
|
88
|
+
:disabled="disable"
|
|
89
|
+
>
|
|
90
|
+
{{ selectedFile ? "Change File" : "Select Excel/CSV File" }}
|
|
91
|
+
</v-btn>
|
|
92
|
+
</v-col>
|
|
93
|
+
|
|
94
|
+
<v-col v-if="selectedFile" cols="12" class="mt-2">
|
|
95
|
+
<v-card variant="outlined">
|
|
96
|
+
<v-card-text class="py-2">
|
|
97
|
+
<v-row no-gutters align="center">
|
|
98
|
+
<v-col>
|
|
99
|
+
<div class="d-flex align-center">
|
|
100
|
+
<v-icon class="me-2" color="success"
|
|
101
|
+
>mdi-file-check</v-icon
|
|
102
|
+
>
|
|
103
|
+
<div>
|
|
104
|
+
<div class="text-subtitle-2 font-weight-medium">
|
|
105
|
+
{{ selectedFile.name }}
|
|
106
|
+
</div>
|
|
107
|
+
<div class="text-caption text-medium-emphasis">
|
|
108
|
+
{{ formatFileSize(selectedFile.size) }}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</v-col>
|
|
113
|
+
<v-col cols="auto">
|
|
114
|
+
<v-btn
|
|
115
|
+
icon="mdi-close"
|
|
116
|
+
variant="text"
|
|
117
|
+
size="small"
|
|
118
|
+
@click="clearFile"
|
|
119
|
+
:disabled="disable"
|
|
120
|
+
></v-btn>
|
|
121
|
+
</v-col>
|
|
122
|
+
</v-row>
|
|
123
|
+
</v-card-text>
|
|
124
|
+
</v-card>
|
|
125
|
+
</v-col>
|
|
126
|
+
|
|
127
|
+
<v-col v-if="!selectedFile" cols="12" class="mt-2">
|
|
128
|
+
<v-alert type="info" variant="tonal" class="text-caption">
|
|
129
|
+
<strong>File Requirements:</strong><br />
|
|
130
|
+
• Supported formats: CSV, Excel (.xlsx, .xls)<br />
|
|
131
|
+
• Required columns: schoolName, schoolId, district<br />
|
|
132
|
+
• Maximum file size: 16MB<br />
|
|
133
|
+
• Download the template above for the correct format
|
|
134
|
+
</v-alert>
|
|
135
|
+
</v-col>
|
|
136
|
+
|
|
137
|
+
<v-col cols="12" class="my-2">
|
|
138
|
+
<v-row no-gutters>
|
|
139
|
+
<v-col cols="12" class="text-center">
|
|
140
|
+
<span
|
|
141
|
+
class="text-none text-subtitle-2 font-weight-medium text-error"
|
|
142
|
+
>
|
|
143
|
+
{{ message }}
|
|
144
|
+
</span>
|
|
145
|
+
</v-col>
|
|
146
|
+
</v-row>
|
|
147
|
+
</v-col>
|
|
148
|
+
</v-row>
|
|
149
|
+
</v-form>
|
|
150
|
+
</v-card-text>
|
|
151
|
+
|
|
152
|
+
<v-toolbar>
|
|
153
|
+
<v-row class="px-6">
|
|
154
|
+
<v-col cols="6">
|
|
155
|
+
<v-btn
|
|
156
|
+
block
|
|
157
|
+
variant="text"
|
|
158
|
+
class="text-none"
|
|
159
|
+
size="large"
|
|
160
|
+
@click="cancel"
|
|
161
|
+
:disabled="disable"
|
|
162
|
+
>
|
|
163
|
+
Cancel
|
|
164
|
+
</v-btn>
|
|
165
|
+
</v-col>
|
|
166
|
+
|
|
167
|
+
<v-col cols="6">
|
|
168
|
+
<v-btn
|
|
169
|
+
block
|
|
170
|
+
variant="flat"
|
|
171
|
+
color="black"
|
|
172
|
+
class="text-none"
|
|
173
|
+
size="large"
|
|
174
|
+
:disabled="!isFormValid || disable"
|
|
175
|
+
@click="submit"
|
|
176
|
+
:loading="disable"
|
|
177
|
+
>
|
|
178
|
+
Upload Schools
|
|
179
|
+
</v-btn>
|
|
180
|
+
</v-col>
|
|
181
|
+
</v-row>
|
|
182
|
+
</v-toolbar>
|
|
183
|
+
</v-card>
|
|
184
|
+
</template>
|
|
185
|
+
|
|
186
|
+
<script setup lang="ts">
|
|
187
|
+
const emit = defineEmits(["cancel", "success", "success:create-more"]);
|
|
188
|
+
|
|
189
|
+
const validForm = ref(false);
|
|
190
|
+
|
|
191
|
+
const selectedRegion = ref<Record<string, any> | null>(null);
|
|
192
|
+
const selectedDivision = ref<Record<string, any> | null>(null);
|
|
193
|
+
const disable = ref(false);
|
|
194
|
+
|
|
195
|
+
// File upload variables
|
|
196
|
+
const selectedFile = ref<File | null>(null);
|
|
197
|
+
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
198
|
+
|
|
199
|
+
const { requiredRule } = useUtils();
|
|
200
|
+
|
|
201
|
+
const message = ref("");
|
|
202
|
+
|
|
203
|
+
const { bulkAdd } = useSchool();
|
|
204
|
+
|
|
205
|
+
const { getAll: getAllRegions } = useRegion();
|
|
206
|
+
|
|
207
|
+
const regionOptions = ref<Array<Record<string, any>>>([]);
|
|
208
|
+
|
|
209
|
+
const { data: getAllRegionReq, status: getRegionStatus } =
|
|
210
|
+
await useLazyAsyncData("get-all-regions", () =>
|
|
211
|
+
getAllRegions({
|
|
212
|
+
page: 1,
|
|
213
|
+
limit: 100,
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const regionsLoading = computed(() => getRegionStatus.value === "pending");
|
|
218
|
+
|
|
219
|
+
watchEffect(() => {
|
|
220
|
+
if (getAllRegionReq.value) {
|
|
221
|
+
regionOptions.value = getAllRegionReq.value.items;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const { getAll: getDivisions } = useDivision();
|
|
226
|
+
|
|
227
|
+
// Load divisions for selection
|
|
228
|
+
const divisionOptions = ref<Array<Record<string, any>>>([]);
|
|
229
|
+
|
|
230
|
+
const { data: getSDOData, status } = await useLazyAsyncData(
|
|
231
|
+
"get-all-school-division-offices",
|
|
232
|
+
() => getDivisions({ page: 1, limit: 100 })
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
watchEffect(() => {
|
|
236
|
+
if (getSDOData.value) {
|
|
237
|
+
divisionOptions.value = getSDOData.value.items || [];
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const divisionsLoading = computed(() => status.value === "pending");
|
|
242
|
+
|
|
243
|
+
// Form validation - ensure all required fields are filled
|
|
244
|
+
const isFormValid = computed(() => {
|
|
245
|
+
return selectedRegion.value && selectedDivision.value && selectedFile.value;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// File handling methods
|
|
249
|
+
function triggerFileInput() {
|
|
250
|
+
if (fileInputRef.value) {
|
|
251
|
+
fileInputRef.value.click();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function handleFileSelect(event: Event) {
|
|
256
|
+
const target = event.target as HTMLInputElement;
|
|
257
|
+
const file = target.files?.[0];
|
|
258
|
+
|
|
259
|
+
if (file) {
|
|
260
|
+
// Validate file type
|
|
261
|
+
const allowedTypes = [
|
|
262
|
+
"text/csv",
|
|
263
|
+
"application/vnd.ms-excel",
|
|
264
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const allowedExtensions = [".csv", ".xls", ".xlsx"];
|
|
268
|
+
const fileExtension = "." + file.name.split(".").pop()?.toLowerCase();
|
|
269
|
+
|
|
270
|
+
if (
|
|
271
|
+
!allowedTypes.includes(file.type) &&
|
|
272
|
+
!allowedExtensions.includes(fileExtension)
|
|
273
|
+
) {
|
|
274
|
+
message.value = "Please select a valid CSV or Excel file";
|
|
275
|
+
selectedFile.value = null;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Validate file size (16MB limit)
|
|
280
|
+
const maxSize = 16 * 1024 * 1024; // 16MB in bytes
|
|
281
|
+
if (file.size > maxSize) {
|
|
282
|
+
message.value = "File size must be less than 16MB";
|
|
283
|
+
selectedFile.value = null;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
selectedFile.value = file;
|
|
288
|
+
message.value = "";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function clearFile() {
|
|
293
|
+
selectedFile.value = null;
|
|
294
|
+
if (fileInputRef.value) {
|
|
295
|
+
fileInputRef.value.value = "";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatFileSize(bytes: number): string {
|
|
300
|
+
if (bytes === 0) return "0 Bytes";
|
|
301
|
+
|
|
302
|
+
const k = 1024;
|
|
303
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
304
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
305
|
+
|
|
306
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function downloadTemplate() {
|
|
310
|
+
// Create CSV template with the expected format
|
|
311
|
+
const headers = ["schoolName", "schoolId", "district"];
|
|
312
|
+
const sampleData = ["Sample Elementary School", "ELEM001", "District 1"];
|
|
313
|
+
|
|
314
|
+
const csvContent = [
|
|
315
|
+
headers.join(","),
|
|
316
|
+
sampleData.map((field) => `"${field}"`).join(","),
|
|
317
|
+
].join("\n");
|
|
318
|
+
|
|
319
|
+
// Create and download file
|
|
320
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
321
|
+
const link = document.createElement("a");
|
|
322
|
+
const url = URL.createObjectURL(blob);
|
|
323
|
+
|
|
324
|
+
link.setAttribute("href", url);
|
|
325
|
+
link.setAttribute("download", "school-bulk-upload-template.csv");
|
|
326
|
+
link.style.visibility = "hidden";
|
|
327
|
+
|
|
328
|
+
document.body.appendChild(link);
|
|
329
|
+
link.click();
|
|
330
|
+
document.body.removeChild(link);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function submit() {
|
|
334
|
+
disable.value = true;
|
|
335
|
+
try {
|
|
336
|
+
// Validate required fields
|
|
337
|
+
if (
|
|
338
|
+
!selectedRegion.value ||
|
|
339
|
+
!selectedDivision.value ||
|
|
340
|
+
!selectedFile.value
|
|
341
|
+
) {
|
|
342
|
+
message.value = "Please fill in all required fields and select a file";
|
|
343
|
+
disable.value = false;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Call the bulk upload API
|
|
348
|
+
const result = await bulkAdd(
|
|
349
|
+
selectedFile.value,
|
|
350
|
+
selectedRegion.value._id,
|
|
351
|
+
selectedDivision.value._id
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Display success message with details
|
|
355
|
+
const details = result.details;
|
|
356
|
+
let successMessage = result.message;
|
|
357
|
+
|
|
358
|
+
if (details.failed > 0) {
|
|
359
|
+
successMessage += ` ${details.failed} schools failed to upload.`;
|
|
360
|
+
if (details.errors.length > 0) {
|
|
361
|
+
console.warn("Upload errors:", details.errors);
|
|
362
|
+
message.value = `${successMessage} Check console for error details.`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Reset form on success
|
|
367
|
+
selectedRegion.value = null;
|
|
368
|
+
selectedDivision.value = null;
|
|
369
|
+
selectedFile.value = null;
|
|
370
|
+
if (fileInputRef.value) {
|
|
371
|
+
fileInputRef.value.value = "";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Show success and emit event
|
|
375
|
+
if (details.failed === 0) {
|
|
376
|
+
message.value = "";
|
|
377
|
+
emit("success");
|
|
378
|
+
} else {
|
|
379
|
+
// Partial success - show warning but still emit success
|
|
380
|
+
emit("success");
|
|
381
|
+
}
|
|
382
|
+
} catch (error: any) {
|
|
383
|
+
console.error("Bulk upload error:", error);
|
|
384
|
+
message.value =
|
|
385
|
+
error.message ||
|
|
386
|
+
error.response?._data?.message ||
|
|
387
|
+
"Failed to upload schools";
|
|
388
|
+
} finally {
|
|
389
|
+
disable.value = false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function cancel() {
|
|
394
|
+
selectedRegion.value = null;
|
|
395
|
+
selectedDivision.value = null;
|
|
396
|
+
selectedFile.value = null;
|
|
397
|
+
if (fileInputRef.value) {
|
|
398
|
+
fileInputRef.value.value = "";
|
|
399
|
+
}
|
|
400
|
+
message.value = "";
|
|
401
|
+
emit("cancel");
|
|
402
|
+
}
|
|
403
|
+
</script>
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters>
|
|
3
|
+
<v-col cols="12" class="mb-2">
|
|
4
|
+
<v-row no-gutters>
|
|
5
|
+
<v-btn
|
|
6
|
+
class="text-none mr-2"
|
|
7
|
+
rounded="pill"
|
|
8
|
+
variant="tonal"
|
|
9
|
+
@click="dialogCreate = true"
|
|
10
|
+
size="large"
|
|
11
|
+
v-if="canCreate"
|
|
12
|
+
>
|
|
13
|
+
Add School
|
|
14
|
+
</v-btn>
|
|
15
|
+
|
|
16
|
+
<v-btn
|
|
17
|
+
class="text-none"
|
|
18
|
+
rounded="pill"
|
|
19
|
+
variant="tonal"
|
|
20
|
+
@click="dialogUpload = true"
|
|
21
|
+
size="large"
|
|
22
|
+
v-if="canUpload"
|
|
23
|
+
>
|
|
24
|
+
Upload Schools
|
|
25
|
+
</v-btn>
|
|
26
|
+
</v-row>
|
|
27
|
+
</v-col>
|
|
28
|
+
|
|
3
29
|
<v-col cols="12">
|
|
4
30
|
<v-card
|
|
5
31
|
width="100%"
|
|
@@ -54,7 +80,11 @@
|
|
|
54
80
|
hide-default-footer
|
|
55
81
|
hide-default-header
|
|
56
82
|
@click:row="tableRowClickHandler"
|
|
57
|
-
style="max-height:
|
|
83
|
+
:style="`max-height: ${
|
|
84
|
+
canCreate || canUpload
|
|
85
|
+
? 'calc(100vh - (240px))'
|
|
86
|
+
: 'calc(100vh - (195px))'
|
|
87
|
+
} `"
|
|
58
88
|
>
|
|
59
89
|
<template #item.createdAt="{ value }">
|
|
60
90
|
{{ new Date(value).toLocaleDateString() }}
|
|
@@ -64,26 +94,35 @@
|
|
|
64
94
|
</v-col>
|
|
65
95
|
|
|
66
96
|
<!-- Create Dialog -->
|
|
67
|
-
<v-dialog v-model="
|
|
97
|
+
<v-dialog v-model="dialogCreate" width="500" persistent>
|
|
68
98
|
<SchoolFormCreate
|
|
69
|
-
@cancel="
|
|
99
|
+
@cancel="dialogCreate = false"
|
|
100
|
+
@success="successCreate()"
|
|
101
|
+
@success:create-more="getSchools()"
|
|
102
|
+
/>
|
|
103
|
+
</v-dialog>
|
|
104
|
+
|
|
105
|
+
<!-- Upload Dialog -->
|
|
106
|
+
<v-dialog v-model="dialogUpload" width="500" persistent>
|
|
107
|
+
<SchoolFormUpload
|
|
108
|
+
@cancel="dialogUpload = false"
|
|
70
109
|
@success="successCreate()"
|
|
71
110
|
@success:create-more="getSchools()"
|
|
72
111
|
/>
|
|
73
112
|
</v-dialog>
|
|
74
113
|
|
|
75
114
|
<!-- Edit Dialog -->
|
|
76
|
-
<v-dialog v-model="
|
|
115
|
+
<v-dialog v-model="dialogEdit" width="500" persistent>
|
|
77
116
|
<SchoolFormEdit
|
|
78
117
|
v-if="selectedSchool"
|
|
79
|
-
@cancel="
|
|
118
|
+
@cancel="dialogEdit = false"
|
|
80
119
|
@success="successUpdate()"
|
|
81
120
|
:school="selectedSchool"
|
|
82
121
|
/>
|
|
83
122
|
</v-dialog>
|
|
84
123
|
|
|
85
124
|
<!-- Preview Dialog -->
|
|
86
|
-
<v-dialog v-model="
|
|
125
|
+
<v-dialog v-model="dialogPreview" width="500" persistent>
|
|
87
126
|
<v-card width="100%">
|
|
88
127
|
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
|
|
89
128
|
<v-row no-gutters v-if="selectedSchool">
|
|
@@ -134,7 +173,7 @@
|
|
|
134
173
|
class="text-none"
|
|
135
174
|
height="48"
|
|
136
175
|
tile
|
|
137
|
-
@click="
|
|
176
|
+
@click="dialogPreview = false"
|
|
138
177
|
>
|
|
139
178
|
Close
|
|
140
179
|
</v-btn>
|
|
@@ -270,7 +309,11 @@ const prop = defineProps({
|
|
|
270
309
|
},
|
|
271
310
|
canCreate: {
|
|
272
311
|
type: Boolean,
|
|
273
|
-
default:
|
|
312
|
+
default: false,
|
|
313
|
+
},
|
|
314
|
+
canUpload: {
|
|
315
|
+
type: Boolean,
|
|
316
|
+
default: false,
|
|
274
317
|
},
|
|
275
318
|
canUpdate: {
|
|
276
319
|
type: Boolean,
|
|
@@ -322,7 +365,7 @@ const {
|
|
|
322
365
|
refresh: getSchools,
|
|
323
366
|
status: getSchoolReqStatus,
|
|
324
367
|
} = useLazyAsyncData(
|
|
325
|
-
"schools-get-all-" + prop.status,
|
|
368
|
+
"schools-get-all-" + prop.status + "-" + prop.org,
|
|
326
369
|
() =>
|
|
327
370
|
_getSchools({
|
|
328
371
|
page: page.value,
|
|
@@ -348,35 +391,36 @@ watchEffect(() => {
|
|
|
348
391
|
|
|
349
392
|
function tableRowClickHandler(_: any, data: any) {
|
|
350
393
|
selectedSchool.value = data.item;
|
|
351
|
-
|
|
394
|
+
dialogPreview.value = true;
|
|
352
395
|
}
|
|
353
396
|
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
const
|
|
397
|
+
const dialogCreate = ref(false);
|
|
398
|
+
const dialogUpload = ref(false);
|
|
399
|
+
const dialogEdit = ref(false);
|
|
400
|
+
const dialogPreview = ref(false);
|
|
357
401
|
const selectedSchool = ref<Record<string, any> | null>(null);
|
|
358
402
|
|
|
359
403
|
function successCreate() {
|
|
360
|
-
|
|
404
|
+
dialogCreate.value = false;
|
|
361
405
|
getSchools();
|
|
362
406
|
showMessage("School created successfully!", "success");
|
|
363
407
|
}
|
|
364
408
|
|
|
365
409
|
function successUpdate() {
|
|
366
|
-
|
|
367
|
-
|
|
410
|
+
dialogEdit.value = false;
|
|
411
|
+
dialogPreview.value = false;
|
|
368
412
|
getSchools();
|
|
369
413
|
showMessage("School updated successfully!", "success");
|
|
370
414
|
}
|
|
371
415
|
|
|
372
416
|
function openEditDialog(school: Record<string, any>) {
|
|
373
417
|
selectedSchool.value = school;
|
|
374
|
-
|
|
418
|
+
dialogEdit.value = true;
|
|
375
419
|
}
|
|
376
420
|
|
|
377
421
|
function editFromPreview() {
|
|
378
|
-
|
|
379
|
-
|
|
422
|
+
dialogPreview.value = false;
|
|
423
|
+
dialogEdit.value = true;
|
|
380
424
|
}
|
|
381
425
|
|
|
382
426
|
const confirmDialog = ref(false);
|
|
@@ -386,8 +430,8 @@ const deleteLoading = ref(false);
|
|
|
386
430
|
function openDeleteDialog(id: string) {
|
|
387
431
|
selectedSchoolId.value = id;
|
|
388
432
|
confirmDialog.value = true;
|
|
389
|
-
if (
|
|
390
|
-
|
|
433
|
+
if (dialogPreview.value) {
|
|
434
|
+
dialogPreview.value = false;
|
|
391
435
|
}
|
|
392
436
|
}
|
|
393
437
|
|
|
@@ -422,7 +466,7 @@ async function submitApproval() {
|
|
|
422
466
|
error?.response?._data?.message || "Failed to delete school";
|
|
423
467
|
showMessage(errorMessage, "error");
|
|
424
468
|
} finally {
|
|
425
|
-
|
|
469
|
+
dialogPreview.value = false;
|
|
426
470
|
selectedSchoolId.value = null;
|
|
427
471
|
}
|
|
428
472
|
}
|
package/composables/useSchool.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export default function useSchool() {
|
|
2
|
+
function addSchool(school: TSchool) {
|
|
3
|
+
return useNuxtApp().$api<{ message: string }>("/api/schools", {
|
|
4
|
+
method: "POST",
|
|
5
|
+
body: school,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
2
9
|
function registerSchool(school: TSchool) {
|
|
3
10
|
return useNuxtApp().$api("/api/schools/register", {
|
|
4
11
|
method: "POST",
|
|
@@ -37,10 +44,37 @@ export default function useSchool() {
|
|
|
37
44
|
});
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
function bulkAdd(file: File | null, region: string, division: string) {
|
|
48
|
+
if (!file) {
|
|
49
|
+
throw new Error("File not found.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const formData = new FormData();
|
|
53
|
+
formData.append("file", file);
|
|
54
|
+
formData.append("region", region);
|
|
55
|
+
formData.append("division", division);
|
|
56
|
+
|
|
57
|
+
return useNuxtApp().$api<{
|
|
58
|
+
message: string;
|
|
59
|
+
details: {
|
|
60
|
+
successful: number;
|
|
61
|
+
failed: number;
|
|
62
|
+
total: number;
|
|
63
|
+
totalSizeMB: number;
|
|
64
|
+
errors: string[];
|
|
65
|
+
};
|
|
66
|
+
}>("/api/schools/bulk", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: formData,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
return {
|
|
73
|
+
addSchool,
|
|
41
74
|
registerSchool,
|
|
42
75
|
getPendingByCreatedBy,
|
|
43
76
|
getAll,
|
|
44
77
|
approvedById,
|
|
78
|
+
bulkAdd,
|
|
45
79
|
};
|
|
46
80
|
}
|
package/package.json
CHANGED
package/types/school.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
declare type TSchool = {
|
|
2
2
|
_id?: string;
|
|
3
|
-
id
|
|
3
|
+
id?: string;
|
|
4
4
|
name: string;
|
|
5
|
-
country
|
|
6
|
-
address
|
|
7
|
-
continuedAddress
|
|
8
|
-
city
|
|
9
|
-
province
|
|
10
|
-
postalCode
|
|
11
|
-
courses
|
|
12
|
-
principalName
|
|
13
|
-
principalEmail
|
|
14
|
-
principalNumber
|
|
15
|
-
region
|
|
5
|
+
country?: string;
|
|
6
|
+
address?: string;
|
|
7
|
+
continuedAddress?: string;
|
|
8
|
+
city?: string;
|
|
9
|
+
province?: string;
|
|
10
|
+
postalCode?: string;
|
|
11
|
+
courses?: Array<string>;
|
|
12
|
+
principalName?: string;
|
|
13
|
+
principalEmail?: string;
|
|
14
|
+
principalNumber?: string;
|
|
15
|
+
region?: string;
|
|
16
16
|
regionName?: string;
|
|
17
|
-
division
|
|
17
|
+
division?: string;
|
|
18
18
|
divisionName?: string;
|
|
19
19
|
status?: string;
|
|
20
20
|
createdAt?: string;
|