@eeplatform/nuxt-layer-common 1.2.1 → 1.2.3

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @eeplatform/nuxt-layer-common
2
2
 
3
+ ## 1.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 5a927d9: Revise school and invite components
8
+
9
+ ## 1.2.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 49f4a58: Revise authentication implementation
14
+
3
15
  ## 1.2.1
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,206 @@
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"> Invite </span>
6
+ </v-row>
7
+ </v-toolbar>
8
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
9
+ <v-form v-model="validInvite">
10
+ <v-row no-gutters>
11
+ <v-col cols="12">
12
+ <v-row no-gutters>
13
+ <InputLabel class="text-capitalize" title="E-mail" required />
14
+ <v-col cols="12">
15
+ <v-text-field
16
+ v-model="invite.email"
17
+ density="comfortable"
18
+ :rules="[requiredRule, emailRule]"
19
+ ></v-text-field>
20
+ </v-col>
21
+ </v-row>
22
+ </v-col>
23
+
24
+ <v-col cols="12" class="my-2">
25
+ <v-row no-gutters>
26
+ <v-col cols="12">
27
+ <v-input
28
+ :error-messages="invite.role ? [] : ['Role is required']"
29
+ >
30
+ <v-menu z-index="10000">
31
+ <template #activator="{ props }">
32
+ <v-btn
33
+ block
34
+ variant="flat"
35
+ color="black"
36
+ class="text-none"
37
+ height="48"
38
+ v-bind="props"
39
+ >
40
+ {{ invite.role ? roleName : "Select Role" }}
41
+ </v-btn>
42
+ </template>
43
+
44
+ <v-list class="pa-0">
45
+ <v-list-item v-if="!roles.length"> No data </v-list-item>
46
+
47
+ <v-list-item
48
+ v-for="role in roles"
49
+ :key="role._id"
50
+ @click="invite.role = role._id"
51
+ >
52
+ <template #prepend>
53
+ <v-icon v-if="invite.role === role._id">
54
+ mdi-check
55
+ </v-icon>
56
+ </template>
57
+
58
+ <v-list-item-title
59
+ class="text-subtitle-2 font-weight-medium"
60
+ >
61
+ {{ role.name }}
62
+ </v-list-item-title>
63
+ </v-list-item>
64
+ </v-list>
65
+ </v-menu>
66
+ </v-input>
67
+ </v-col>
68
+ </v-row>
69
+ </v-col>
70
+
71
+ <v-col v-if="messageInvite" cols="12" class="text-center mb-4">
72
+ <span
73
+ class="text-none text-subtitle-2 font-weight-medium text-error"
74
+ >
75
+ {{ messageInvite }}
76
+ </span>
77
+ </v-col>
78
+ </v-row>
79
+ </v-form>
80
+ </v-card-text>
81
+
82
+ <v-toolbar class="pa-0" density="compact">
83
+ <v-row no-gutters>
84
+ <v-col cols="6" class="pa-0">
85
+ <v-btn
86
+ block
87
+ variant="text"
88
+ class="text-none"
89
+ size="large"
90
+ @click="cancel()"
91
+ height="48"
92
+ tile
93
+ >
94
+ Cancel
95
+ </v-btn>
96
+ </v-col>
97
+
98
+ <v-col cols="6">
99
+ <v-btn
100
+ block
101
+ variant="flat"
102
+ color="black"
103
+ class="text-none"
104
+ size="large"
105
+ @click="submitInvite()"
106
+ height="48"
107
+ tile
108
+ :disabled="!validInvite"
109
+ >
110
+ Submit
111
+ </v-btn>
112
+ </v-col>
113
+ </v-row>
114
+ </v-toolbar>
115
+ </v-card>
116
+ </template>
117
+
118
+ <script setup lang="ts">
119
+ const prop = defineProps({
120
+ app: {
121
+ type: String,
122
+ required: true,
123
+ default: "admin",
124
+ },
125
+ org: {
126
+ type: String,
127
+ default: "",
128
+ },
129
+ orgName: {
130
+ type: String,
131
+ default: "",
132
+ },
133
+ });
134
+
135
+ const { requiredRule, emailRule } = useUtils();
136
+
137
+ const invite = ref({
138
+ email: "",
139
+ app: "",
140
+ role: "",
141
+ });
142
+
143
+ const searchRole = ref("");
144
+ const roles = ref<Array<Record<string, any>>>([]);
145
+
146
+ const { getRoles } = useRole();
147
+
148
+ const { data: getRolesData, status: getRolesStatus } = await useLazyAsyncData(
149
+ "get-roles",
150
+ () =>
151
+ getRoles({
152
+ search: searchRole.value,
153
+ type: prop.app,
154
+ id: prop.org,
155
+ limit: 20,
156
+ })
157
+ );
158
+
159
+ const loading = computed(() => {
160
+ return getRolesStatus.value === "pending";
161
+ });
162
+
163
+ watchEffect(() => {
164
+ if (getRolesData.value) {
165
+ roles.value = getRolesData.value.items;
166
+ }
167
+ });
168
+
169
+ const validInvite = ref(false);
170
+
171
+ const { inviteUser } = useUser();
172
+
173
+ const messageInvite = ref("");
174
+
175
+ const roleName = computed(() => {
176
+ return roles.value.find((i) => i._id === invite.value.role)?.name || "";
177
+ });
178
+
179
+ const emit = defineEmits(["success", "cancel"]);
180
+
181
+ async function submitInvite() {
182
+ messageInvite.value = "";
183
+ try {
184
+ await inviteUser({
185
+ email: invite.value.email,
186
+ app: prop.app,
187
+ role: invite.value.role,
188
+ roleName: roleName.value,
189
+ org: prop.org,
190
+ orgName: prop.orgName,
191
+ });
192
+
193
+ await emit("success");
194
+ } catch (error: any) {
195
+ messageInvite.value =
196
+ error.response?._data?.message || "Failed to invite user";
197
+ }
198
+ }
199
+
200
+ function cancel() {
201
+ invite.value.email = "";
202
+ invite.value.app = prop.app;
203
+ invite.value.role = "";
204
+ emit("cancel");
205
+ }
206
+ </script>
@@ -6,7 +6,7 @@
6
6
  class="text-none mr-2"
7
7
  rounded="pill"
8
8
  variant="tonal"
9
- :to="props.inviteRoute"
9
+ @click="dialogInvite = true"
10
10
  size="large"
11
11
  v-if="props.inviteMember"
12
12
  >
@@ -46,20 +46,14 @@
46
46
  <template #extension>
47
47
  <v-tabs>
48
48
  <v-tab
49
+ v-for="status in statusFilter"
50
+ :key="status.text"
49
51
  :to="{
50
- name: 'org-organization-invitations-status-status',
51
- params: { status: 'pending', organization },
52
+ name: props.baseRoute,
53
+ params: status.params,
52
54
  }"
53
55
  >
54
- Pending
55
- </v-tab>
56
- <v-tab
57
- :to="{
58
- name: 'org-organization-invitations-status-status',
59
- params: { status: 'expired', organization },
60
- }"
61
- >
62
- Expired
56
+ {{ status.text }}
63
57
  </v-tab>
64
58
  </v-tabs>
65
59
  </template>
@@ -86,6 +80,15 @@
86
80
  </v-data-table>
87
81
  </v-card>
88
82
  </v-col>
83
+
84
+ <v-dialog v-model="dialogInvite" width="400" persistent>
85
+ <InvitationForm
86
+ :app="props.app"
87
+ :org="props.org"
88
+ @cancel="dialogInvite = false"
89
+ @success="inviteSuccess()"
90
+ />
91
+ </v-dialog>
89
92
  </v-row>
90
93
  </template>
91
94
 
@@ -95,6 +98,10 @@ const props = defineProps({
95
98
  type: String,
96
99
  default: "active",
97
100
  },
101
+ org: {
102
+ type: String,
103
+ default: "",
104
+ },
98
105
  app: {
99
106
  type: String,
100
107
  default: "organization",
@@ -103,6 +110,10 @@ const props = defineProps({
103
110
  type: Boolean,
104
111
  default: false,
105
112
  },
113
+ baseRoute: {
114
+ type: String,
115
+ default: "invitations-status-status",
116
+ },
106
117
  inviteRoute: {
107
118
  type: Object as PropType<Record<string, any>>,
108
119
  default: () => ({
@@ -112,8 +123,6 @@ const props = defineProps({
112
123
  },
113
124
  });
114
125
 
115
- const organization = (useRoute().params.organization as string) ?? "";
116
-
117
126
  const headers = [
118
127
  {
119
128
  title: "Date",
@@ -129,6 +138,22 @@ const headers = [
129
138
  },
130
139
  ];
131
140
 
141
+ const statusFilter = computed(() => {
142
+ const items = [
143
+ { text: "Pending", params: { status: "pending" } },
144
+ { text: "Expired", params: { status: "expired" } },
145
+ ];
146
+
147
+ if (props.org) {
148
+ items.map((i) => ({
149
+ ...i,
150
+ params: { ...i.params, organization: props.org },
151
+ }));
152
+ }
153
+
154
+ return items;
155
+ });
156
+
132
157
  const { getVerifications } = useVerification();
133
158
 
134
159
  const page = ref(1);
@@ -186,6 +211,13 @@ watch(selectAll, (curr) => {
186
211
  selected.value.push(...ids);
187
212
  }
188
213
  });
214
+
215
+ const dialogInvite = ref(false);
216
+
217
+ function inviteSuccess() {
218
+ dialogInvite.value = false;
219
+ getInvitations();
220
+ }
189
221
  </script>
190
222
 
191
223
  <style scoped>
@@ -293,11 +293,6 @@ const props = defineProps({
293
293
 
294
294
  value: "roleName",
295
295
  },
296
- {
297
- title: "Organization",
298
-
299
- value: "orgName",
300
- },
301
296
  {
302
297
  title: "Action",
303
298
  value: "action-table",
@@ -379,7 +374,7 @@ function setMember({
379
374
  const roles = ref<Array<Record<string, any>>>([]);
380
375
  const { getRoles } = useRole();
381
376
  const { data: getAllRoleReq } = useLazyAsyncData("get-roles", () =>
382
- getRoles({ org: props.orgId, type: props.type, limit: 20 })
377
+ getRoles({ id: props.orgId, type: props.type, limit: 20 })
383
378
  );
384
379
 
385
380
  watchEffect(() => {
@@ -168,7 +168,6 @@ const props = defineProps({
168
168
  title: "permissions",
169
169
  value: "permissions",
170
170
  },
171
- { title: "App", value: "type" },
172
171
  { title: "Action", value: "action-table" },
173
172
  ],
174
173
  },
@@ -0,0 +1,216 @@
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
+ Create School
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+ <v-card-text style="max-height: 100vh; overflow-y: auto">
11
+ <v-form v-model="validForm" :disabled="disable">
12
+ <v-row no-gutters>
13
+ <v-col cols="12" class="mt-2">
14
+ <v-row no-gutters>
15
+ <InputLabel class="text-capitalize" title="School Name" required />
16
+ <v-col cols="12">
17
+ <v-text-field
18
+ v-model="name"
19
+ density="comfortable"
20
+ :rules="[requiredRule]"
21
+ placeholder="Enter school name"
22
+ ></v-text-field>
23
+ </v-col>
24
+ </v-row>
25
+ </v-col>
26
+
27
+ <v-col cols="12" class="mt-2">
28
+ <v-row no-gutters>
29
+ <InputLabel class="text-capitalize" title="Division" required />
30
+ <v-col cols="12">
31
+ <v-select
32
+ v-model="selectedDivision"
33
+ :items="divisionOptions"
34
+ item-title="name"
35
+ item-value="_id"
36
+ density="comfortable"
37
+ :rules="[requiredRule]"
38
+ placeholder="Select a division"
39
+ :loading="divisionsLoading"
40
+ return-object
41
+ ></v-select>
42
+ </v-col>
43
+ </v-row>
44
+ </v-col>
45
+
46
+ <v-col cols="12" class="mt-2">
47
+ <v-row no-gutters>
48
+ <InputLabel class="text-capitalize" title="Principal Name" />
49
+ <v-col cols="12">
50
+ <v-text-field
51
+ v-model="principalName"
52
+ density="comfortable"
53
+ placeholder="Enter principal name (optional)"
54
+ ></v-text-field>
55
+ </v-col>
56
+ </v-row>
57
+ </v-col>
58
+
59
+ <v-col cols="12" class="mt-2">
60
+ <v-row no-gutters>
61
+ <InputLabel class="text-capitalize" title="Address" />
62
+ <v-col cols="12">
63
+ <v-textarea
64
+ v-model="address"
65
+ density="comfortable"
66
+ placeholder="Enter school address (optional)"
67
+ rows="3"
68
+ ></v-textarea>
69
+ </v-col>
70
+ </v-row>
71
+ </v-col>
72
+
73
+ <v-col cols="12" class="mt-2">
74
+ <v-checkbox v-model="createMore" density="comfortable" hide-details>
75
+ <template #label>
76
+ <span class="text-subtitle-2 font-weight-bold">
77
+ Create more
78
+ </span>
79
+ </template>
80
+ </v-checkbox>
81
+ </v-col>
82
+
83
+ <v-col cols="12" class="my-2">
84
+ <v-row no-gutters>
85
+ <v-col cols="12" class="text-center">
86
+ <span
87
+ class="text-none text-subtitle-2 font-weight-medium text-error"
88
+ >
89
+ {{ message }}
90
+ </span>
91
+ </v-col>
92
+ </v-row>
93
+ </v-col>
94
+ </v-row>
95
+ </v-form>
96
+ </v-card-text>
97
+
98
+ <v-toolbar>
99
+ <v-row class="px-6">
100
+ <v-col cols="6">
101
+ <v-btn
102
+ block
103
+ variant="text"
104
+ class="text-none"
105
+ size="large"
106
+ @click="cancel"
107
+ :disabled="disable"
108
+ >
109
+ Cancel
110
+ </v-btn>
111
+ </v-col>
112
+
113
+ <v-col cols="6">
114
+ <v-btn
115
+ block
116
+ variant="flat"
117
+ color="black"
118
+ class="text-none"
119
+ size="large"
120
+ :disabled="!validForm || disable"
121
+ @click="submit"
122
+ :loading="disable"
123
+ >
124
+ Create School
125
+ </v-btn>
126
+ </v-col>
127
+ </v-row>
128
+ </v-toolbar>
129
+ </v-card>
130
+ </template>
131
+
132
+ <script setup lang="ts">
133
+ const emit = defineEmits(["cancel", "success", "success:create-more"]);
134
+
135
+ const validForm = ref(false);
136
+
137
+ const name = ref("");
138
+ const selectedDivision = ref<Record<string, any> | null>(null);
139
+ const principalName = ref("");
140
+ const address = ref("");
141
+ const createMore = ref(false);
142
+ const disable = ref(false);
143
+
144
+ const { requiredRule } = useUtils();
145
+
146
+ const message = ref("");
147
+
148
+ const { createSchool } = useSchool();
149
+ const { getAll: getDivisions } = useDivision();
150
+
151
+ // Load divisions for selection
152
+ const divisionOptions = ref<Array<Record<string, any>>>([]);
153
+ const divisionsLoading = ref(false);
154
+
155
+ onMounted(async () => {
156
+ divisionsLoading.value = true;
157
+ try {
158
+ const response = await getDivisions({ page: 1, limit: 100 });
159
+ divisionOptions.value = response.items || [];
160
+ } catch (error) {
161
+ console.error('Failed to load divisions:', error);
162
+ } finally {
163
+ divisionsLoading.value = false;
164
+ }
165
+ });
166
+
167
+ async function submit() {
168
+ disable.value = true;
169
+ try {
170
+ const payload: Record<string, any> = {
171
+ name: name.value,
172
+ };
173
+
174
+ if (selectedDivision.value) {
175
+ payload.division = selectedDivision.value._id;
176
+ payload.divisionName = selectedDivision.value.name;
177
+ }
178
+
179
+ if (principalName.value.trim()) {
180
+ payload.principalName = principalName.value.trim();
181
+ }
182
+
183
+ if (address.value.trim()) {
184
+ payload.address = address.value.trim();
185
+ }
186
+
187
+ await createSchool(payload);
188
+
189
+ if (createMore.value) {
190
+ name.value = "";
191
+ selectedDivision.value = null;
192
+ principalName.value = "";
193
+ address.value = "";
194
+ message.value = "";
195
+ emit("success:create-more");
196
+ return;
197
+ }
198
+
199
+ emit("success");
200
+ } catch (error: any) {
201
+ message.value = error.response?._data?.message || "Failed to create school";
202
+ } finally {
203
+ disable.value = false;
204
+ }
205
+ }
206
+
207
+ function cancel() {
208
+ name.value = "";
209
+ selectedDivision.value = null;
210
+ principalName.value = "";
211
+ address.value = "";
212
+ createMore.value = false;
213
+ message.value = "";
214
+ emit("cancel");
215
+ }
216
+ </script>
@@ -0,0 +1,177 @@
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"> Edit School </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="School Name" required />
14
+ <v-col cols="12">
15
+ <v-text-field
16
+ v-model="name"
17
+ density="comfortable"
18
+ :rules="[requiredRule]"
19
+ placeholder="Enter school name"
20
+ ></v-text-field>
21
+ </v-col>
22
+ </v-row>
23
+ </v-col>
24
+
25
+ <v-col cols="12" class="mt-2">
26
+ <v-row no-gutters>
27
+ <InputLabel class="text-capitalize" title="Principal Name" />
28
+ <v-col cols="12">
29
+ <v-text-field
30
+ v-model="principalName"
31
+ density="comfortable"
32
+ placeholder="Enter principal name (optional)"
33
+ ></v-text-field>
34
+ </v-col>
35
+ </v-row>
36
+ </v-col>
37
+
38
+ <v-col cols="12" class="mt-2">
39
+ <v-row no-gutters>
40
+ <InputLabel class="text-capitalize" title="Address" />
41
+ <v-col cols="12">
42
+ <v-textarea
43
+ v-model="address"
44
+ density="comfortable"
45
+ placeholder="Enter school address (optional)"
46
+ rows="3"
47
+ ></v-textarea>
48
+ </v-col>
49
+ </v-row>
50
+ </v-col>
51
+
52
+ <v-col cols="12" class="my-2">
53
+ <v-row no-gutters>
54
+ <v-col cols="12" class="text-center">
55
+ <span
56
+ class="text-none text-subtitle-2 font-weight-medium text-error"
57
+ >
58
+ {{ message }}
59
+ </span>
60
+ </v-col>
61
+ </v-row>
62
+ </v-col>
63
+ </v-row>
64
+ </v-form>
65
+ </v-card-text>
66
+
67
+ <v-toolbar>
68
+ <v-row class="px-6">
69
+ <v-col cols="6">
70
+ <v-btn
71
+ block
72
+ variant="text"
73
+ class="text-none"
74
+ size="large"
75
+ @click="cancel"
76
+ :disabled="disable"
77
+ >
78
+ Cancel
79
+ </v-btn>
80
+ </v-col>
81
+
82
+ <v-col cols="6">
83
+ <v-btn
84
+ block
85
+ variant="flat"
86
+ color="black"
87
+ class="text-none"
88
+ size="large"
89
+ :disabled="!validForm || disable || !hasChanges"
90
+ @click="submit"
91
+ :loading="disable"
92
+ >
93
+ Update School
94
+ </v-btn>
95
+ </v-col>
96
+ </v-row>
97
+ </v-toolbar>
98
+ </v-card>
99
+ </template>
100
+
101
+ <script setup lang="ts">
102
+ const props = defineProps({
103
+ school: {
104
+ type: Object as PropType<Record<string, any>>,
105
+ required: true,
106
+ },
107
+ });
108
+
109
+ const emit = defineEmits(["cancel", "success"]);
110
+
111
+ const validForm = ref(false);
112
+
113
+ const name = ref("");
114
+ const principalName = ref("");
115
+ const address = ref("");
116
+ const disable = ref(false);
117
+
118
+ const { requiredRule } = useUtils();
119
+
120
+ const message = ref("");
121
+
122
+ const { updateSchoolField } = useSchool();
123
+
124
+ // Initialize form with existing data
125
+ watchEffect(() => {
126
+ if (props.school) {
127
+ name.value = props.school.name || "";
128
+ principalName.value = props.school.principalName || "";
129
+ address.value = props.school.address || "";
130
+ }
131
+ });
132
+
133
+ const hasChanges = computed(() => {
134
+ if (!props.school) return false;
135
+ return (
136
+ name.value !== (props.school.name || "") ||
137
+ principalName.value !== (props.school.principalName || "") ||
138
+ address.value !== (props.school.address || "")
139
+ );
140
+ });
141
+
142
+ async function submit() {
143
+ if (!props.school?._id) return;
144
+
145
+ disable.value = true;
146
+ try {
147
+ // Update fields that have changed
148
+ if (name.value !== (props.school.name || "")) {
149
+ await updateSchoolField(props.school._id, "name", name.value);
150
+ }
151
+
152
+ if (principalName.value !== (props.school.principalName || "")) {
153
+ await updateSchoolField(props.school._id, "principalName", principalName.value);
154
+ }
155
+
156
+ if (address.value !== (props.school.address || "")) {
157
+ await updateSchoolField(props.school._id, "address", address.value);
158
+ }
159
+
160
+ emit("success");
161
+ } catch (error: any) {
162
+ message.value = error.response?._data?.message || "Failed to update school";
163
+ } finally {
164
+ disable.value = false;
165
+ }
166
+ }
167
+
168
+ function cancel() {
169
+ // Reset to original values
170
+ name.value = props.school?.name || "";
171
+ principalName.value = props.school?.principalName || "";
172
+ address.value = props.school?.address || "";
173
+ message.value = "";
174
+ emit("cancel");
175
+ }
176
+ </script>
177
+
@@ -0,0 +1,429 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12">
4
+ <v-card
5
+ width="100%"
6
+ variant="outlined"
7
+ border="thin"
8
+ rounded="lg"
9
+ :loading="loading"
10
+ >
11
+ <v-toolbar density="compact" color="grey-lighten-4">
12
+ <template #prepend>
13
+ <v-btn fab icon density="comfortable" @click="getSchools()">
14
+ <v-icon>mdi-refresh</v-icon>
15
+ </v-btn>
16
+ </template>
17
+
18
+ <template #append>
19
+ <v-row no-gutters justify="end" align="center">
20
+ <span class="mr-2 text-caption text-fontgray">
21
+ {{ pageRange }}
22
+ </span>
23
+ <local-pagination
24
+ v-model="page"
25
+ :length="pages"
26
+ @update:value="getSchools()"
27
+ />
28
+ </v-row>
29
+ </template>
30
+
31
+ <template #extension>
32
+ <v-tabs v-model="theStatus">
33
+ <v-tab
34
+ v-for="status in statusFilter"
35
+ :key="status.text"
36
+ :to="{
37
+ name: prop.baseRoute,
38
+ params: status.params,
39
+ }"
40
+ class="text-capitalize"
41
+ >
42
+ {{ status.text }}
43
+ </v-tab>
44
+ </v-tabs>
45
+ </template>
46
+ </v-toolbar>
47
+
48
+ <v-data-table
49
+ :headers="headers"
50
+ :items="items"
51
+ item-value="_id"
52
+ items-per-page="20"
53
+ fixed-header
54
+ hide-default-footer
55
+ hide-default-header
56
+ @click:row="tableRowClickHandler"
57
+ style="max-height: calc(100vh - (180px))"
58
+ >
59
+ <template #item.createdAt="{ value }">
60
+ {{ new Date(value).toLocaleDateString() }}
61
+ </template>
62
+ </v-data-table>
63
+ </v-card>
64
+ </v-col>
65
+
66
+ <!-- Create Dialog -->
67
+ <v-dialog v-model="createDialog" width="500" persistent>
68
+ <SchoolFormCreate
69
+ @cancel="createDialog = false"
70
+ @success="successCreate()"
71
+ @success:create-more="getSchools()"
72
+ />
73
+ </v-dialog>
74
+
75
+ <!-- Edit Dialog -->
76
+ <v-dialog v-model="editDialog" width="500" persistent>
77
+ <SchoolFormEdit
78
+ v-if="selectedSchool"
79
+ @cancel="editDialog = false"
80
+ @success="successUpdate()"
81
+ :school="selectedSchool"
82
+ />
83
+ </v-dialog>
84
+
85
+ <!-- Preview Dialog -->
86
+ <v-dialog v-model="previewDialog" width="500" persistent>
87
+ <v-card width="100%">
88
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
89
+ <v-row no-gutters v-if="selectedSchool">
90
+ <v-col cols="12" class="mb-3">
91
+ <strong>Name:</strong> {{ selectedSchool.name }}
92
+ </v-col>
93
+ <v-col cols="12" class="mb-3">
94
+ <strong>Address:</strong>
95
+ {{
96
+ `${selectedSchool.address} ${
97
+ selectedSchool.continuedAddress || ""
98
+ }`
99
+ }}
100
+ {{ selectedSchool.city }} {{ selectedSchool.state }}
101
+ {{ selectedSchool.zipCode }}
102
+ </v-col>
103
+ <v-col cols="12" class="mb-3">
104
+ <strong>Region:</strong>
105
+ {{ selectedSchool.regionName || "Not assigned" }}
106
+ </v-col>
107
+ <v-col cols="12" class="mb-3">
108
+ <strong>Division:</strong>
109
+ {{ selectedSchool.divisionName || "Not assigned" }}
110
+ </v-col>
111
+ <v-col cols="12" class="mb-3">
112
+ <strong>Principal:</strong>
113
+ {{ selectedSchool.principalName || "Not assigned" }}
114
+ </v-col>
115
+ <v-col cols="12" class="mb-3">
116
+ <strong>Created:</strong>
117
+ {{
118
+ new Date(selectedSchool.createdAt || "").toLocaleDateString()
119
+ }}
120
+ </v-col>
121
+ <v-col cols="12" class="mb-3" v-if="selectedSchool.updatedAt">
122
+ <strong>Updated:</strong>
123
+ {{ new Date(selectedSchool.updatedAt).toLocaleDateString() }}
124
+ </v-col>
125
+ </v-row>
126
+ </v-card-text>
127
+
128
+ <v-toolbar class="pa-0" density="compact">
129
+ <v-row no-gutter>
130
+ <v-col cols="6" class="pa-0">
131
+ <v-btn
132
+ block
133
+ variant="text"
134
+ class="text-none"
135
+ height="48"
136
+ tile
137
+ @click="previewDialog = false"
138
+ >
139
+ Close
140
+ </v-btn>
141
+ </v-col>
142
+
143
+ <v-col cols="6" class="pa-0" v-if="canUpdate">
144
+ <v-menu>
145
+ <template #activator="{ props }">
146
+ <v-btn
147
+ block
148
+ variant="flat"
149
+ color="black"
150
+ class="text-none"
151
+ height="48"
152
+ v-bind="props"
153
+ tile
154
+ >
155
+ More actions
156
+ </v-btn>
157
+ </template>
158
+
159
+ <v-list class="pa-0">
160
+ <v-list-item
161
+ v-if="selectedSchool?.status === 'pending'"
162
+ @click="submitApproval()"
163
+ >
164
+ <v-list-item-title class="text-subtitle-2">
165
+ Approve School
166
+ </v-list-item-title>
167
+ </v-list-item>
168
+
169
+ <v-list-item @click="editFromPreview()">
170
+ <v-list-item-title class="text-subtitle-2">
171
+ Edit School
172
+ </v-list-item-title>
173
+ </v-list-item>
174
+
175
+ <v-list-item
176
+ @click="openDeleteDialog(selectedSchool?._id)"
177
+ class="text-red"
178
+ >
179
+ <v-list-item-title class="text-subtitle-2">
180
+ Delete School
181
+ </v-list-item-title>
182
+ </v-list-item>
183
+ </v-list>
184
+ </v-menu>
185
+ </v-col>
186
+ </v-row>
187
+ </v-toolbar>
188
+ </v-card>
189
+ </v-dialog>
190
+
191
+ <ConfirmDialog
192
+ v-model="confirmDialog"
193
+ :loading="deleteLoading"
194
+ @submit="handleDeleteSchool"
195
+ >
196
+ <template #title>
197
+ <span class="font-weight-medium text-h5">Delete School</span>
198
+ </template>
199
+
200
+ <template #description>
201
+ <p class="text-subtitle-2">
202
+ Are you sure you want to delete this school? This action cannot be
203
+ undone.
204
+ </p>
205
+ </template>
206
+
207
+ <template #footer>
208
+ <v-btn
209
+ variant="text"
210
+ @click="confirmDialog = false"
211
+ :disabled="deleteLoading"
212
+ class="text-none"
213
+ >
214
+ Close
215
+ </v-btn>
216
+ <v-btn
217
+ color="black"
218
+ variant="flat"
219
+ @click="handleDeleteSchool"
220
+ :loading="deleteLoading"
221
+ class="text-none"
222
+ >
223
+ Delete School
224
+ </v-btn>
225
+ </template>
226
+ </ConfirmDialog>
227
+
228
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
229
+ </v-row>
230
+ </template>
231
+
232
+ <script setup lang="ts">
233
+ const prop = defineProps({
234
+ status: {
235
+ type: String,
236
+ default: "active",
237
+ },
238
+ org: {
239
+ type: String,
240
+ default: "",
241
+ },
242
+ app: {
243
+ type: String,
244
+ default: "school",
245
+ },
246
+ baseRoute: {
247
+ type: String,
248
+ default: "index",
249
+ },
250
+ headers: {
251
+ type: Array as PropType<Array<Record<string, any>>>,
252
+ default: () => [
253
+ {
254
+ title: "Name",
255
+ value: "name",
256
+ },
257
+ {
258
+ title: "Region",
259
+ value: "regionName",
260
+ },
261
+ {
262
+ title: "Division",
263
+ value: "divisionName",
264
+ },
265
+ {
266
+ title: "Principal",
267
+ value: "principalName",
268
+ },
269
+ ],
270
+ },
271
+ canCreate: {
272
+ type: Boolean,
273
+ default: true,
274
+ },
275
+ canUpdate: {
276
+ type: Boolean,
277
+ default: true,
278
+ },
279
+ canDelete: {
280
+ type: Boolean,
281
+ default: true,
282
+ },
283
+ });
284
+
285
+ const statusFilter = computed(() => {
286
+ const items = [
287
+ { text: "Active", params: { status: "active" } },
288
+ { text: "Pending", params: { status: "pending" } },
289
+ { text: "Suspended", params: { status: "suspended" } },
290
+ ];
291
+
292
+ if (prop.org) {
293
+ items.map((i) => ({
294
+ ...i,
295
+ params: { ...i.params, org: prop.org },
296
+ }));
297
+ }
298
+
299
+ return items;
300
+ });
301
+
302
+ const statuses = ["active", "pending", "suspended", "inactive"];
303
+ const theStatus = ref("active");
304
+
305
+ const { headerSearch } = useLocal();
306
+ const { getAll: _getSchools, approvedById } = useSchool();
307
+
308
+ const page = ref(1);
309
+ const pages = ref(0);
310
+ const pageRange = ref("-- - -- of --");
311
+
312
+ const message = ref("");
313
+ const messageSnackbar = ref(false);
314
+ const messageColor = ref("");
315
+
316
+ const items = ref<Array<Record<string, any>>>([]);
317
+
318
+ const propStatus = computed(() => prop.status);
319
+
320
+ const {
321
+ data: getSchoolReq,
322
+ refresh: getSchools,
323
+ status: getSchoolReqStatus,
324
+ } = useLazyAsyncData(
325
+ "schools-get-all-" + prop.status,
326
+ () =>
327
+ _getSchools({
328
+ page: page.value,
329
+ search: headerSearch.value,
330
+ status: prop.status,
331
+ org: prop.org,
332
+ app: prop.app,
333
+ }),
334
+ {
335
+ watch: [page, propStatus],
336
+ }
337
+ );
338
+
339
+ const loading = computed(() => getSchoolReqStatus.value === "pending");
340
+
341
+ watchEffect(() => {
342
+ if (getSchoolReq.value) {
343
+ items.value = getSchoolReq.value.items;
344
+ pages.value = getSchoolReq.value.pages;
345
+ pageRange.value = getSchoolReq.value.pageRange;
346
+ }
347
+ });
348
+
349
+ function tableRowClickHandler(_: any, data: any) {
350
+ selectedSchool.value = data.item;
351
+ previewDialog.value = true;
352
+ }
353
+
354
+ const createDialog = ref(false);
355
+ const editDialog = ref(false);
356
+ const previewDialog = ref(false);
357
+ const selectedSchool = ref<Record<string, any> | null>(null);
358
+
359
+ function successCreate() {
360
+ createDialog.value = false;
361
+ getSchools();
362
+ showMessage("School created successfully!", "success");
363
+ }
364
+
365
+ function successUpdate() {
366
+ editDialog.value = false;
367
+ previewDialog.value = false;
368
+ getSchools();
369
+ showMessage("School updated successfully!", "success");
370
+ }
371
+
372
+ function openEditDialog(school: Record<string, any>) {
373
+ selectedSchool.value = school;
374
+ editDialog.value = true;
375
+ }
376
+
377
+ function editFromPreview() {
378
+ previewDialog.value = false;
379
+ editDialog.value = true;
380
+ }
381
+
382
+ const confirmDialog = ref(false);
383
+ const selectedSchoolId = ref<string | null>(null);
384
+ const deleteLoading = ref(false);
385
+
386
+ function openDeleteDialog(id: string) {
387
+ selectedSchoolId.value = id;
388
+ confirmDialog.value = true;
389
+ if (previewDialog.value) {
390
+ previewDialog.value = false;
391
+ }
392
+ }
393
+
394
+ function showMessage(msg: string, color: string) {
395
+ message.value = msg;
396
+ messageColor.value = color;
397
+ messageSnackbar.value = true;
398
+ }
399
+
400
+ async function handleDeleteSchool() {
401
+ if (!selectedSchoolId.value) return;
402
+ deleteLoading.value = true;
403
+ try {
404
+ confirmDialog.value = false;
405
+ getSchools();
406
+ } catch (error: any) {
407
+ const errorMessage =
408
+ error?.response?._data?.message || "Failed to delete school";
409
+ showMessage(errorMessage, "error");
410
+ } finally {
411
+ deleteLoading.value = false;
412
+ selectedSchoolId.value = null;
413
+ }
414
+ }
415
+
416
+ async function submitApproval() {
417
+ try {
418
+ await approvedById(selectedSchool.value?._id ?? "");
419
+ getSchools();
420
+ } catch (error: any) {
421
+ const errorMessage =
422
+ error?.response?._data?.message || "Failed to delete school";
423
+ showMessage(errorMessage, "error");
424
+ } finally {
425
+ previewDialog.value = false;
426
+ selectedSchoolId.value = null;
427
+ }
428
+ }
429
+ </script>
@@ -30,19 +30,19 @@ export default function useLocal() {
30
30
  title: "School",
31
31
  icon: "ph:building-bold",
32
32
  link: APP_SCHOOL as string,
33
- landingPage: "home",
33
+ landingPage: "",
34
34
  },
35
35
  {
36
36
  title: "Division",
37
37
  icon: "ph:building-apartment-bold",
38
38
  link: APP_DIVISION as string,
39
- landingPage: "home",
39
+ landingPage: "",
40
40
  },
41
41
  {
42
42
  title: "Region",
43
43
  icon: "ph:buildings-bold",
44
44
  link: APP_REGION as string,
45
- landingPage: "home",
45
+ landingPage: "",
46
46
  },
47
47
  ];
48
48
  });
@@ -38,7 +38,7 @@ export default function useLocalAuth() {
38
38
  }
39
39
 
40
40
  async function login({ email = "", password = "", role = "" }) {
41
- return useNuxtApp().$api<TKeyValuePair>("/api/auth/login", {
41
+ return useNuxtApp().$api<Record<string, any>>("/api/auth/login", {
42
42
  method: "POST",
43
43
  body: JSON.stringify({ email, password, role }),
44
44
  });
@@ -78,19 +78,17 @@ export default function useLocalAuth() {
78
78
  }
79
79
  }
80
80
 
81
- async function getCurrentUser() {
81
+ function getCurrentUser() {
82
82
  const user = useCookie("user", cookieConfig).value;
83
- if (!user) return null;
84
- try {
85
- const _user = await useNuxtApp().$api<TUser>(`/api/users/id/${user}`, {
86
- method: "GET",
87
- });
83
+ const { data: userData } = useLazyAsyncData("get-current-user", () =>
84
+ useNuxtApp().$api<TUser>(`/api/users/id/${user}`)
85
+ );
88
86
 
89
- currentUser.value = _user;
90
- return _user;
91
- } catch (error) {
92
- console.log("Error fetching current user:", error);
93
- }
87
+ watchEffect(() => {
88
+ if (userData.value) {
89
+ currentUser.value = userData.value;
90
+ }
91
+ });
94
92
  }
95
93
 
96
94
  async function forgotPassword(email: string) {
@@ -129,13 +127,13 @@ export default function useLocalAuth() {
129
127
  }
130
128
 
131
129
  function verify(id: string) {
132
- return useNuxtApp().$api<TKeyValuePair>(`/api/auth/verify/${id}`, {
130
+ return useNuxtApp().$api<Record<string, any>>(`/api/auth/verify/${id}`, {
133
131
  method: "GET",
134
132
  });
135
133
  }
136
134
 
137
135
  function signUp(email: string, referral: string) {
138
- return useNuxtApp().$api<TKeyValuePair>("/api/auth/sign-up", {
136
+ return useNuxtApp().$api<Record<string, any>>("/api/auth/sign-up", {
139
137
  method: "POST",
140
138
  body: { email, referral },
141
139
  });
@@ -1,14 +1,12 @@
1
1
  export function useLocalSetup(type: string, org?: string) {
2
- const { currentUser } = useLocalAuth();
3
-
4
- const userId = computed(() => currentUser.value?._id ?? "");
2
+ const userId = computed(() => useCookie("user").value ?? "");
5
3
 
6
4
  const { getByUserType } = useMember();
7
5
 
8
6
  const { data: userMemberData, error: userMemberError } = useLazyAsyncData(
9
7
  "get-member-by-id",
10
8
  () => getByUserType(userId.value, type, org),
11
- { watch: [userId], immediate: false }
9
+ { watch: [userId] }
12
10
  );
13
11
 
14
12
  watchEffect(() => {
@@ -40,7 +38,10 @@ export function useLocalSetup(type: string, org?: string) {
40
38
  }
41
39
  });
42
40
 
41
+ const id = computed(() => userMemberData.value?.org ?? "id");
42
+
43
43
  return {
44
44
  userAppRole,
45
+ id,
45
46
  };
46
47
  }
@@ -28,12 +28,12 @@ export default function useSchool() {
28
28
  page = 1,
29
29
  status = "active",
30
30
  search = "",
31
- region = "",
32
- division = "",
31
+ org = "",
32
+ app = "admin",
33
33
  } = {}) {
34
34
  return useNuxtApp().$api<Record<string, any>>("/api/schools", {
35
35
  method: "GET",
36
- query: { page, status, search, region, division },
36
+ query: { page, status, search, org, app },
37
37
  });
38
38
  }
39
39
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@eeplatform/nuxt-layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.2.1",
5
+ "version": "1.2.3",
6
6
  "main": "./nuxt.config.ts",
7
7
  "publishConfig": {
8
8
  "access": "public"