@eeplatform/nuxt-layer-common 1.5.0 → 1.6.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,597 @@
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">
6
+ {{ props.title }}
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+
11
+ <v-card-text style="max-height: 100vh; overflow-y: auto">
12
+ <v-form v-model="formValid" :disabled="disable">
13
+ <v-row no-gutters>
14
+ <v-col cols="12" class="mt-2">
15
+ <v-row no-gutters>
16
+ <InputLabel
17
+ class="text-capitalize"
18
+ title="Curriculum"
19
+ :required="props.mode !== 'view'"
20
+ />
21
+ <v-col cols="12">
22
+ <v-combobox
23
+ v-model="curriculumSubject.curriculum"
24
+ v-model:search="searchCurriculum"
25
+ density="comfortable"
26
+ :rules="[requiredRule]"
27
+ :return-object="false"
28
+ :items="curriculums"
29
+ item-title="name"
30
+ item-value="_id"
31
+ :error-messages="errorCurriculum"
32
+ :readonly="props.mode === 'view'"
33
+ >
34
+ <template v-slot:no-data>
35
+ <v-list-item>
36
+ <v-list-item-title>
37
+ No results matching "<strong>{{
38
+ searchCurriculum
39
+ }}</strong
40
+ >". Click the search button and try again.
41
+ </v-list-item-title>
42
+ </v-list-item>
43
+ </template>
44
+
45
+ <template #append>
46
+ <v-btn
47
+ variant="tonal"
48
+ :disabled="curriculumReqStatus"
49
+ class="text-none"
50
+ size="48"
51
+ width="100"
52
+ icon
53
+ @click="_getAllCurriculum()"
54
+ >
55
+ <v-icon>ph:magnifying-glass-bold</v-icon>
56
+ </v-btn>
57
+ </template>
58
+ </v-combobox>
59
+ </v-col>
60
+ </v-row>
61
+ </v-col>
62
+
63
+ <v-col cols="12" class="mt-2">
64
+ <v-row no-gutters>
65
+ <InputLabel
66
+ class="text-capitalize"
67
+ title="Education Level"
68
+ :required="props.mode !== 'view'"
69
+ />
70
+ <v-col cols="12">
71
+ <v-combobox
72
+ v-model="curriculumSubject.educationLevel"
73
+ density="comfortable"
74
+ :rules="[requiredRule]"
75
+ :return-object="false"
76
+ :items="educationLevels"
77
+ :readonly="props.mode === 'view'"
78
+ ></v-combobox>
79
+ </v-col>
80
+ </v-row>
81
+ </v-col>
82
+
83
+ <v-col cols="12" class="mt-2">
84
+ <v-row no-gutters>
85
+ <InputLabel
86
+ class="text-capitalize"
87
+ title="Grade Level"
88
+ :required="props.mode !== 'view'"
89
+ />
90
+ <v-col cols="12">
91
+ <v-autocomplete
92
+ v-model="curriculumSubject.gradeLevel"
93
+ density="comfortable"
94
+ :rules="[requiredRule]"
95
+ :items="selectedGradeLevels"
96
+ :readonly="props.mode === 'view'"
97
+ multiple
98
+ chips
99
+ ></v-autocomplete>
100
+ </v-col>
101
+ </v-row>
102
+ </v-col>
103
+
104
+ <v-col cols="12" class="mt-2">
105
+ <v-row no-gutters>
106
+ <InputLabel
107
+ class="text-capitalize"
108
+ title="Subject Code"
109
+ :required="props.mode !== 'view'"
110
+ />
111
+ <v-col cols="12">
112
+ <v-text-field
113
+ v-model="curriculumSubject.subjectCode"
114
+ density="comfortable"
115
+ :rules="[requiredRule]"
116
+ :readonly="props.mode === 'view'"
117
+ ></v-text-field>
118
+ </v-col>
119
+ </v-row>
120
+ </v-col>
121
+
122
+ <v-col cols="12" class="mt-2">
123
+ <v-row no-gutters>
124
+ <InputLabel
125
+ class="text-capitalize"
126
+ title="Subject Name"
127
+ :required="props.mode !== 'view'"
128
+ />
129
+ <v-col cols="12">
130
+ <v-text-field
131
+ v-model="curriculumSubject.subjectName"
132
+ density="comfortable"
133
+ :rules="[requiredRule]"
134
+ :readonly="props.mode === 'view'"
135
+ ></v-text-field>
136
+ </v-col>
137
+ </v-row>
138
+ </v-col>
139
+
140
+ <v-col cols="12" class="mt-2">
141
+ <v-row no-gutters>
142
+ <InputLabel
143
+ class="text-capitalize"
144
+ title="Subject Type"
145
+ :required="props.mode !== 'view'"
146
+ />
147
+ <v-col cols="12">
148
+ <v-autocomplete
149
+ v-model="curriculumSubject.subjectType"
150
+ density="comfortable"
151
+ :rules="[requiredRule]"
152
+ :items="subjectTypes"
153
+ :readonly="props.mode === 'view'"
154
+ ></v-autocomplete>
155
+ </v-col>
156
+ </v-row>
157
+ </v-col>
158
+
159
+ <v-col cols="12" class="mt-2">
160
+ <v-row no-gutters>
161
+ <InputLabel
162
+ class="text-capitalize"
163
+ title="Session Frequency (Classes per week)"
164
+ :required="props.mode !== 'view'"
165
+ />
166
+ <v-col cols="12">
167
+ <v-combobox
168
+ v-model.number="curriculumSubject.sessionFrequency"
169
+ :items="sessionPerWeekOptions"
170
+ density="comfortable"
171
+ :rules="[requiredRule]"
172
+ type="number"
173
+ :readonly="props.mode === 'view'"
174
+ ></v-combobox>
175
+ </v-col>
176
+ </v-row>
177
+ </v-col>
178
+
179
+ <v-col cols="12" class="mt-2">
180
+ <v-row no-gutters>
181
+ <InputLabel
182
+ class="text-capitalize"
183
+ title="Session Duration (Minutes per class)"
184
+ :required="props.mode !== 'view'"
185
+ />
186
+ <v-col cols="12">
187
+ <v-combobox
188
+ v-model.number="curriculumSubject.sessionDuration"
189
+ density="comfortable"
190
+ :rules="[requiredRule]"
191
+ :items="sessionDurationOptions"
192
+ type="number"
193
+ :readonly="props.mode === 'view'"
194
+ ></v-combobox>
195
+ </v-col>
196
+ </v-row>
197
+ </v-col>
198
+
199
+ <v-col v-if="props.mode === 'create'" cols="12">
200
+ <v-checkbox v-model="createMore" density="comfortable" hide-details>
201
+ <template #label>
202
+ <span class="text-subtitle-2 font-weight-bold">
203
+ Create more
204
+ </span>
205
+ </template>
206
+ </v-checkbox>
207
+ </v-col>
208
+
209
+ <v-col v-if="message" cols="12" class="my-2">
210
+ <v-row no-gutters>
211
+ <v-col cols="12" class="text-center">
212
+ <span
213
+ class="text-none text-subtitle-2 font-weight-medium text-error"
214
+ >
215
+ {{ message }}
216
+ </span>
217
+ </v-col>
218
+ </v-row>
219
+ </v-col>
220
+ </v-row>
221
+ </v-form>
222
+ </v-card-text>
223
+
224
+ <v-toolbar density="compact">
225
+ <v-row no-gutters>
226
+ <v-col
227
+ v-if="modeViewEdit && (props.canEdit || props.canDelete)"
228
+ :cols="props.canEdit || props.canDelete ? 6 : 12"
229
+ >
230
+ <v-btn
231
+ tile
232
+ block
233
+ variant="text"
234
+ class="text-none"
235
+ size="48"
236
+ @click="emit('cancel')"
237
+ >
238
+ Close
239
+ </v-btn>
240
+ </v-col>
241
+
242
+ <v-col
243
+ v-if="modeViewEdit && props.canEdit && !props.canDelete"
244
+ :cols="props.canEdit ? 6 : 12"
245
+ >
246
+ <v-btn
247
+ tile
248
+ block
249
+ variant="flat"
250
+ color="black"
251
+ class="text-none"
252
+ size="48"
253
+ @click="emit('cancel')"
254
+ >
255
+ Edit curriculum
256
+ </v-btn>
257
+ </v-col>
258
+
259
+ <v-col
260
+ v-if="modeViewEdit && props.canDelete && !props.canEdit"
261
+ :cols="props.canDelete ? 6 : 12"
262
+ >
263
+ <v-btn
264
+ tile
265
+ block
266
+ variant="flat"
267
+ class="text-none text-error"
268
+ size="48"
269
+ @click="emit('cancel')"
270
+ >
271
+ Delete curriculum
272
+ </v-btn>
273
+ </v-col>
274
+
275
+ <v-col v-if="hasMoreActions" cols="6">
276
+ <v-menu>
277
+ <template #activator="{ props }">
278
+ <v-btn
279
+ block
280
+ variant="flat"
281
+ color="black"
282
+ class="text-none"
283
+ height="48"
284
+ v-bind="props"
285
+ tile
286
+ >
287
+ More actions
288
+ </v-btn>
289
+ </template>
290
+
291
+ <v-list class="pa-0">
292
+ <v-list-item @click="emit('edit')">
293
+ <v-list-item-title class="text-subtitle-2">
294
+ Edit curriculum
295
+ </v-list-item-title>
296
+ </v-list-item>
297
+
298
+ <v-list-item @click="emit('delete')" class="text-red">
299
+ <v-list-item-title class="text-subtitle-2">
300
+ Delete curriculum
301
+ </v-list-item-title>
302
+ </v-list-item>
303
+ </v-list>
304
+ </v-menu>
305
+ </v-col>
306
+
307
+ <v-col v-if="props.mode === 'edit'" cols="6">
308
+ <v-btn
309
+ tile
310
+ block
311
+ variant="flat"
312
+ color="black"
313
+ class="text-none"
314
+ size="48"
315
+ :disabled="!formValid"
316
+ @click="submitUpdate"
317
+ >
318
+ Submit update
319
+ </v-btn>
320
+ </v-col>
321
+
322
+ <v-col v-if="props.mode === 'create'" cols="6">
323
+ <v-btn
324
+ tile
325
+ block
326
+ variant="text"
327
+ class="text-none"
328
+ size="48"
329
+ @click="emit('cancel')"
330
+ >
331
+ Cancel
332
+ </v-btn>
333
+ </v-col>
334
+
335
+ <v-col v-if="props.mode === 'create'" cols="6">
336
+ <v-btn
337
+ tile
338
+ block
339
+ variant="flat"
340
+ color="black"
341
+ class="text-none"
342
+ size="48"
343
+ :disabled="!formValid"
344
+ @click="submit"
345
+ >
346
+ Submit
347
+ </v-btn>
348
+ </v-col>
349
+ </v-row>
350
+ </v-toolbar>
351
+ </v-card>
352
+ </template>
353
+
354
+ <script setup lang="ts">
355
+ const props = defineProps({
356
+ title: {
357
+ type: String,
358
+ default: "Subject Form",
359
+ },
360
+ mode: {
361
+ type: String,
362
+ default: "create",
363
+ },
364
+ data: {
365
+ type: Object as PropType<TCurriculumSubject>,
366
+ default: () => ({}),
367
+ },
368
+ canEdit: {
369
+ type: Boolean,
370
+ default: false,
371
+ },
372
+ canDelete: {
373
+ type: Boolean,
374
+ default: false,
375
+ },
376
+ });
377
+
378
+ const emit = defineEmits([
379
+ "cancel",
380
+ "edit",
381
+ "delete",
382
+ "success",
383
+ "success:create-more",
384
+ ]);
385
+
386
+ const formValid = ref(false);
387
+
388
+ const curriculums = ref<Array<TCurriculum>>([]);
389
+ const searchCurriculum = ref("");
390
+
391
+ const { getAll: getAllCurriculum } = useCurriculum();
392
+
393
+ const {
394
+ data: getCurriculumReq,
395
+ refresh: _getAllCurriculum,
396
+ status: getAllCurriculumStatus,
397
+ } = await useLazyAsyncData(
398
+ `curriculums-as-options-list-${searchCurriculum.value ?? "search"}`,
399
+ () =>
400
+ getAllCurriculum({
401
+ limit: 20,
402
+ search: searchCurriculum.value,
403
+ })
404
+ );
405
+
406
+ const curriculumReqStatus = computed(
407
+ () => getAllCurriculumStatus.value === "pending"
408
+ );
409
+
410
+ watchEffect(() => {
411
+ if (getCurriculumReq.value) {
412
+ curriculums.value = getCurriculumReq.value.items;
413
+ }
414
+ });
415
+
416
+ const curriculumSubject = ref<TCurriculumSubject>({
417
+ educationLevel: "",
418
+ gradeLevel: [],
419
+ subjectCode: "",
420
+ subjectName: "",
421
+ subjectType: "",
422
+ sessionFrequency: 0,
423
+ sessionDuration: 0,
424
+ totalMinutesPerWeek: 0,
425
+ curriculum: "",
426
+ curriculumName: "",
427
+ });
428
+
429
+ watchEffect(() => {
430
+ if (curriculumSubject.value.curriculum) {
431
+ const selected = curriculums.value.find(
432
+ (curr) => curr._id === curriculumSubject.value.curriculum
433
+ );
434
+
435
+ if (selected) {
436
+ curriculumSubject.value.curriculumName = selected.name;
437
+ curriculumSubject.value.effectiveSchoolYear = selected.effectiveSchoolYear
438
+ .split("-")
439
+ .map((year: any) => parseInt(year));
440
+ } else {
441
+ curriculumSubject.value.curriculumName = "";
442
+ curriculumSubject.value.effectiveSchoolYear = [];
443
+ }
444
+ }
445
+ });
446
+
447
+ const selectedCurriculum = computed(() =>
448
+ curriculums.value.find(
449
+ (curr) => curr._id === curriculumSubject.value.curriculum
450
+ )
451
+ );
452
+
453
+ const errorCurriculum = computed(() => {
454
+ return !selectedCurriculum.value && curriculumSubject.value.curriculum
455
+ ? ["Please select a valid curriculum"]
456
+ : [];
457
+ });
458
+
459
+ if (["view", "edit"].includes(props.mode) && props.data) {
460
+ Object.assign(
461
+ curriculumSubject.value,
462
+ JSON.parse(JSON.stringify(props.data))
463
+ );
464
+ }
465
+
466
+ watchEffect(() => {
467
+ curriculumSubject.value.totalMinutesPerWeek =
468
+ curriculumSubject.value.sessionFrequency *
469
+ curriculumSubject.value.sessionDuration;
470
+ });
471
+
472
+ const subjectHeaders = [
473
+ { title: "Education Level", value: "educationLevel", width: "140px" },
474
+ { title: "Grade Level", value: "gradeLevel", width: "120px" },
475
+ { title: "Code", value: "subjectCode", width: "100px" },
476
+ { title: "Name", value: "subjectName" },
477
+ { title: "Type", value: "subjectType", width: "100px" },
478
+ { title: "Session Frequency", value: "sessionFrequency", width: "100px" },
479
+ { title: "Session Duration", value: "sessionDuration", width: "100px" },
480
+ { title: "Minutes Per Week", value: "totalMinutesPerWeek", width: "120px" },
481
+ ];
482
+
483
+ const dialogSubject = ref(false);
484
+ const formSubject = ref(false);
485
+
486
+ const { generateSchoolYears, gradeLevels } = useBasicEdu();
487
+
488
+ const effectiveSchoolYearOptions = computed(() =>
489
+ generateSchoolYears(5, "future")
490
+ );
491
+
492
+ const selectedGradeLevels = computed(() => {
493
+ return curriculumSubject.value.educationLevel
494
+ ? gradeLevels.filter(
495
+ (level) => level.level === curriculumSubject.value.educationLevel
496
+ )
497
+ : [];
498
+ });
499
+
500
+ const { educationLevels } = useBasicEdu();
501
+
502
+ const subjectTypes = ["Core", "Applied", "Specialized", "Elective", "Other"];
503
+
504
+ const sessionPerWeekOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
505
+
506
+ const sessionDurationOptions = [30, 35, 40, 45, 50, 55, 60, 75, 90, 120];
507
+
508
+ const createMore = ref(false);
509
+ const disable = ref(false);
510
+
511
+ const { requiredRule } = useUtils();
512
+
513
+ const message = ref("");
514
+ const type = defineModel("type", {
515
+ type: String,
516
+ default: "",
517
+ });
518
+
519
+ const { add, updateById } = useCurriculumSubject();
520
+
521
+ async function submit() {
522
+ disable.value = true;
523
+ try {
524
+ if (props.mode === "create") {
525
+ await add(curriculumSubject.value);
526
+ }
527
+
528
+ console.log(props.mode === "edit");
529
+
530
+ if (props.mode === "edit") {
531
+ await updateById(curriculumSubject.value._id ?? "", {
532
+ curriculum: curriculumSubject.value.curriculum,
533
+ educationLevel: curriculumSubject.value.educationLevel,
534
+ gradeLevel: curriculumSubject.value.gradeLevel,
535
+ subjectCode: curriculumSubject.value.subjectCode,
536
+ subjectName: curriculumSubject.value.subjectName,
537
+ subjectType: curriculumSubject.value.subjectType,
538
+ sessionFrequency: curriculumSubject.value.sessionFrequency,
539
+ sessionDuration: curriculumSubject.value.sessionDuration,
540
+ totalMinutesPerWeek: curriculumSubject.value.totalMinutesPerWeek,
541
+ });
542
+ }
543
+
544
+ if (createMore.value) {
545
+ message.value = "";
546
+ emit("success:create-more");
547
+ return;
548
+ }
549
+
550
+ emit("success");
551
+ } catch (error: any) {
552
+ message.value = error.response._data.message;
553
+ } finally {
554
+ disable.value = false;
555
+ }
556
+ }
557
+
558
+ const canAdd = computed(() => {
559
+ return ["create", "edit"].includes(props.mode);
560
+ });
561
+
562
+ async function submitUpdate() {
563
+ disable.value = true;
564
+ try {
565
+ await updateById(curriculumSubject.value._id ?? "", {
566
+ curriculum: curriculumSubject.value.curriculum,
567
+ educationLevel: curriculumSubject.value.educationLevel,
568
+ gradeLevel: curriculumSubject.value.gradeLevel,
569
+ subjectCode: curriculumSubject.value.subjectCode,
570
+ subjectName: curriculumSubject.value.subjectName,
571
+ subjectType: curriculumSubject.value.subjectType,
572
+ sessionFrequency: curriculumSubject.value.sessionFrequency,
573
+ sessionDuration: curriculumSubject.value.sessionDuration,
574
+ totalMinutesPerWeek: curriculumSubject.value.totalMinutesPerWeek,
575
+ });
576
+ if (createMore.value) {
577
+ message.value = "";
578
+ emit("success:create-more");
579
+ return;
580
+ }
581
+
582
+ emit("success");
583
+ } catch (error: any) {
584
+ message.value = error.response._data.message;
585
+ } finally {
586
+ disable.value = false;
587
+ }
588
+ }
589
+
590
+ const modeViewEdit = computed(() => {
591
+ return ["view", "edit"].includes(props.mode);
592
+ });
593
+
594
+ const hasMoreActions = computed(() => {
595
+ return props.mode === "view" && props.canEdit && props.canDelete;
596
+ });
597
+ </script>
@@ -1486,8 +1486,11 @@ const isSeniorHighSchool = computed(() => {
1486
1486
  return ["grade-11", "grade-12"].includes(gradeLevel);
1487
1487
  });
1488
1488
 
1489
- const effectiveSchoolYearOptions = generateSchoolYears();
1490
- const lastSchoolYearOptions = generateSchoolYears(50).reverse();
1489
+ const effectiveSchoolYearOptions = computed(() =>
1490
+ generateSchoolYears(0, "future")
1491
+ );
1492
+
1493
+ const lastSchoolYearOptions = generateSchoolYears(50);
1491
1494
 
1492
1495
  const indigenousCommunitiesPhilippines = [
1493
1496
  // Luzon (Northern Philippines) - Cordillera Groups (Igorot Subgroups)
@@ -1664,7 +1667,6 @@ watchEffect(() => {
1664
1667
  });
1665
1668
 
1666
1669
  import { useMask } from "vuetify";
1667
- import { fa } from "zod/v4/locales";
1668
1670
 
1669
1671
  watchEffect(() => {
1670
1672
  const mask = useMask({ mask: "##/##/####" });
@@ -114,12 +114,24 @@ export default function useBasicEdu() {
114
114
  },
115
115
  ];
116
116
 
117
- function generateSchoolYears(generation = 0) {
117
+ function generateSchoolYears(generation = 0, mode = "past") {
118
118
  const currentYear = new Date().getFullYear();
119
119
  const years = [];
120
- for (let i = currentYear - generation; i <= currentYear; i++) {
121
- years.push(`${i}-${i + 1}`);
120
+
121
+ if (mode === "past") {
122
+ // Generate from current year going back to past years
123
+ for (let i = 0; i <= generation; i++) {
124
+ const year = currentYear - i;
125
+ years.push(`${year}-${year + 1}`);
126
+ }
127
+ } else if (mode === "future") {
128
+ // Generate from current year going forward to future years
129
+ for (let i = 0; i <= generation; i++) {
130
+ const year = currentYear + i;
131
+ years.push(`${year}-${year + 1}`);
132
+ }
122
133
  }
134
+
123
135
  return years;
124
136
  }
125
137
 
@@ -0,0 +1,49 @@
1
+ export default function useCurriculum() {
2
+ const resource = "basic-education/curriculums";
3
+
4
+ function add(value: TCurriculum) {
5
+ return $fetch<Record<string, any>>(`/api/${resource}`, {
6
+ method: "POST",
7
+ body: value,
8
+ });
9
+ }
10
+
11
+ async function getAll({
12
+ page = 1,
13
+ search = "",
14
+ limit = 10,
15
+ status = "active",
16
+ school = "",
17
+ } = {}) {
18
+ return $fetch<Record<string, any>>(`/api/${resource}`, {
19
+ method: "GET",
20
+ query: { page, limit, search, status, school },
21
+ });
22
+ }
23
+
24
+ function updateById(
25
+ id: string,
26
+ value: Pick<
27
+ TCurriculum,
28
+ "name" | "effectiveSchoolYear" | "maxTeachingHoursPerDay"
29
+ >
30
+ ) {
31
+ return $fetch<Record<string, any>>(`/api/${resource}/${id}`, {
32
+ method: "PUT",
33
+ body: value,
34
+ });
35
+ }
36
+
37
+ function deleteById(id: string) {
38
+ return $fetch<Record<string, any>>(`/api/${resource}/${id}`, {
39
+ method: "DELETE",
40
+ });
41
+ }
42
+
43
+ return {
44
+ add,
45
+ getAll,
46
+ updateById,
47
+ deleteById,
48
+ };
49
+ }