@goweekdays/layer-common 1.6.9 → 1.6.10

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,11 @@
1
1
  # @goweekdays/layer-common
2
2
 
3
+ ## 1.6.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 19f032f: Add UI components for category and tag management (CategoryForm.vue, CategoryMain.vue, TagForm.vue) including dialogs, pagination, and debounced searches. Update useCategory composable to accept an optional org parameter and route requests to /api/asset/item/categories/org/:org when provided (add and getAll). Update useTag composable and TTag type: expand tag shape with normalizedName, type, orgId, categoryPath, status, usageCount; adjust add, getAll and updateById signatures to support the new fields and filters; remove unused parent/children helpers. These changes enable org-scoped category operations and a richer tag model used by the new components.
8
+
3
9
  ## 1.6.9
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,336 @@
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">{{ localProps.title }}</span>
6
+ </v-row>
7
+ </v-toolbar>
8
+
9
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-2">
10
+ <v-form v-model="valid">
11
+ <v-row no-gutters>
12
+ <v-col v-if="isCategory || isSubcategory" cols="12" class="mb-2">
13
+ <v-row no-gutters>
14
+ <InputLabel title="Department" :required="isMutable" />
15
+ <v-col cols="12">
16
+ <v-autocomplete
17
+ v-model="department"
18
+ v-model:search="searchDepartment"
19
+ :items="departments"
20
+ item-title="displayName"
21
+ item-value="_id"
22
+ :return-object="false"
23
+ :readonly="!isMutable"
24
+ :loading="loadingDepartments"
25
+ :rules="isMutable ? [requiredRule] : []"
26
+ :clearable="isMutable"
27
+ ></v-autocomplete>
28
+ </v-col>
29
+ </v-row>
30
+ </v-col>
31
+
32
+ <v-col v-if="isSubcategory" cols="12" class="mb-2">
33
+ <v-row no-gutters>
34
+ <InputLabel title="Category" :required="isMutable" />
35
+ <v-col cols="12">
36
+ <v-autocomplete
37
+ v-model="categoryParent"
38
+ v-model:search="searchCategory"
39
+ :items="categoryParents"
40
+ item-title="displayName"
41
+ item-value="_id"
42
+ :return-object="false"
43
+ :readonly="!isMutable"
44
+ :loading="loadingCategoryParents"
45
+ :rules="isMutable ? [requiredRule] : []"
46
+ :disabled="!department"
47
+ :clearable="isMutable"
48
+ ></v-autocomplete>
49
+ </v-col>
50
+ </v-row>
51
+ </v-col>
52
+
53
+ <v-col cols="12" class="mb-2">
54
+ <v-row no-gutters>
55
+ <InputLabel title="Name" :required="isMutable" />
56
+ <v-col cols="12">
57
+ <v-text-field
58
+ v-model="category.displayName"
59
+ :rules="isMutable ? [requiredRule] : []"
60
+ :readonly="!isMutable"
61
+ ></v-text-field>
62
+ </v-col>
63
+ </v-row>
64
+ </v-col>
65
+ </v-row>
66
+ </v-form>
67
+
68
+ <v-alert
69
+ v-if="message"
70
+ type="error"
71
+ variant="flat"
72
+ closable
73
+ position="absolute"
74
+ location="bottom"
75
+ style="bottom: 48px"
76
+ @click:close="message = ''"
77
+ width="100%"
78
+ tile
79
+ class="text-caption"
80
+ >
81
+ {{ message }}
82
+ </v-alert>
83
+ </v-card-text>
84
+
85
+ <v-toolbar density="compact">
86
+ <v-row no-gutters>
87
+ <v-col cols="6">
88
+ <v-btn
89
+ tile
90
+ block
91
+ variant="text"
92
+ class="text-none"
93
+ size="48"
94
+ @click="emits('close')"
95
+ :disabled="localProps.loading"
96
+ >
97
+ {{ isMutable ? "Cancel" : "Close" }}
98
+ </v-btn>
99
+ </v-col>
100
+
101
+ <v-col v-if="localProps.mode === 'view'" cols="6">
102
+ <v-menu>
103
+ <template #activator="{ props }">
104
+ <v-btn
105
+ block
106
+ variant="flat"
107
+ color="black"
108
+ class="text-none"
109
+ height="48"
110
+ v-bind="props"
111
+ tile
112
+ >
113
+ More actions
114
+ </v-btn>
115
+ </template>
116
+
117
+ <v-list class="pa-0">
118
+ <v-list-item @click="emits('edit')">
119
+ <v-list-item-title class="text-subtitle-2">
120
+ Edit
121
+ </v-list-item-title>
122
+ </v-list-item>
123
+
124
+ <v-list-item @click="emits('delete')" class="text-red">
125
+ <v-list-item-title class="text-subtitle-2">
126
+ Delete
127
+ </v-list-item-title>
128
+ </v-list-item>
129
+ </v-list>
130
+ </v-menu>
131
+ </v-col>
132
+
133
+ <v-col v-if="isMutable" cols="6">
134
+ <v-btn
135
+ tile
136
+ block
137
+ variant="flat"
138
+ color="black"
139
+ class="text-none"
140
+ size="48"
141
+ @click="emits('submit')"
142
+ :loading="localProps.loading"
143
+ :disabled="!valid"
144
+ >
145
+ {{ submitTitle }}
146
+ </v-btn>
147
+ </v-col>
148
+ </v-row>
149
+ </v-toolbar>
150
+ </v-card>
151
+ </template>
152
+
153
+ <script setup lang="ts">
154
+ const localProps = defineProps({
155
+ org: {
156
+ type: String,
157
+ default: "",
158
+ },
159
+ title: {
160
+ type: String,
161
+ default: "Category Form",
162
+ },
163
+ mode: {
164
+ type: String,
165
+ default: "add",
166
+ },
167
+ loading: {
168
+ type: Boolean,
169
+ default: false,
170
+ },
171
+ level: {
172
+ type: String,
173
+ default: "department",
174
+ },
175
+ });
176
+
177
+ const isMutable = computed(() => ["add", "edit"].includes(localProps.mode));
178
+
179
+ const isDepartment = computed(() => localProps.level === "department");
180
+ const isCategory = computed(() => localProps.level === "category");
181
+ const isSubcategory = computed(() => localProps.level === "subcategory");
182
+
183
+ const submitTitle = computed(() => {
184
+ switch (localProps.mode) {
185
+ case "add":
186
+ return "Submit";
187
+ case "edit":
188
+ return "Save changes";
189
+ default:
190
+ return "";
191
+ }
192
+ });
193
+
194
+ const emits = defineEmits(["edit", "delete", "submit", "cancel", "close"]);
195
+
196
+ const category = defineModel({
197
+ type: Object as PropType<TCategory>,
198
+ default: () => useCategory().category.value,
199
+ });
200
+
201
+ const valid = ref(false);
202
+
203
+ const { getAll: getAllCategories, getById } = useCategory();
204
+
205
+ const department = ref("");
206
+
207
+ if (isCategory.value) {
208
+ department.value = category.value.parentId || "";
209
+ }
210
+
211
+ const departments = ref<TCategory[]>([]);
212
+ const categoryParent = ref("");
213
+
214
+ watchEffect(() => {
215
+ if (!department.value) {
216
+ categoryParent.value = "";
217
+ }
218
+
219
+ if (department.value) {
220
+ category.value.parentId = department.value;
221
+ } else if (isSubcategory.value) {
222
+ category.value.parentId = "";
223
+ }
224
+ });
225
+
226
+ watchEffect(() => {
227
+ if (categoryParent.value) {
228
+ category.value.parentId = categoryParent.value;
229
+ } else {
230
+ category.value.parentId = "";
231
+ }
232
+ });
233
+
234
+ if (isSubcategory.value) {
235
+ categoryParent.value = category.value.parentId || "";
236
+
237
+ try {
238
+ const parentCategory = await getById(category.value.parentId ?? "");
239
+ if (parentCategory) {
240
+ department.value = parentCategory.parentId || "";
241
+ }
242
+ } catch (error: any) {
243
+ console.log(
244
+ error.response._data.message || "Error fetching parent category details."
245
+ );
246
+ }
247
+ }
248
+
249
+ const categoryParents = ref<TCategory[]>([]);
250
+
251
+ const message = defineModel("message", { type: String, default: "" });
252
+
253
+ const { requiredRule, debounce } = useUtils();
254
+
255
+ const searchDepartment = ref("");
256
+
257
+ const {
258
+ data: dataDepartments,
259
+ status: statusDepartments,
260
+ refresh: refreshDepartments,
261
+ } = await useLazyAsyncData(
262
+ `get-departments-for-category-form-${localProps.org}`,
263
+ () =>
264
+ getAllCategories({
265
+ page: 1,
266
+ limit: 100,
267
+ level: "department",
268
+ search: searchDepartment.value,
269
+ org: localProps.org,
270
+ })
271
+ );
272
+
273
+ const debouncedSearchDepartment = debounce(() => {
274
+ refreshDepartments();
275
+ }, 500);
276
+
277
+ watch(searchDepartment, (val) => {
278
+ if (val !== searchDepartment.value) {
279
+ debouncedSearchDepartment();
280
+ }
281
+ });
282
+
283
+ const loadingDepartments = computed(
284
+ () => statusDepartments.value === "pending"
285
+ );
286
+
287
+ watchEffect(() => {
288
+ if (dataDepartments.value) {
289
+ departments.value = dataDepartments.value.items;
290
+ }
291
+ });
292
+
293
+ const searchCategory = ref("");
294
+
295
+ const {
296
+ data: dataCategories,
297
+ status: statusCategories,
298
+ refresh: refreshCategories,
299
+ } = await useLazyAsyncData(
300
+ `get-categories-for-category-form-${department.value}-${localProps.org}`,
301
+ () =>
302
+ getAllCategories({
303
+ page: 1,
304
+ limit: 100,
305
+ level: "category",
306
+ parent: department.value,
307
+ search: searchCategory.value,
308
+ org: localProps.org,
309
+ })
310
+ );
311
+
312
+ const debouncedSearchCategory = debounce(() => {
313
+ refreshCategories();
314
+ }, 500);
315
+
316
+ watch(searchCategory, (val) => {
317
+ if (val !== searchCategory.value) {
318
+ debouncedSearchCategory();
319
+ }
320
+ });
321
+
322
+ watch(department, () => {
323
+ categoryParent.value = "";
324
+ refreshCategories();
325
+ });
326
+
327
+ const loadingCategoryParents = computed(
328
+ () => statusCategories.value === "pending"
329
+ );
330
+
331
+ watchEffect(() => {
332
+ if (dataCategories.value) {
333
+ categoryParents.value = dataCategories.value.items;
334
+ }
335
+ });
336
+ </script>
@@ -0,0 +1,292 @@
1
+ <template>
2
+ <v-col cols="12">
3
+ <v-row no-gutters>
4
+ <v-col cols="12" class="mb-2">
5
+ <v-row no-gutters align="center">
6
+ <v-btn
7
+ class="text-none mr-2"
8
+ rounded="pill"
9
+ variant="tonal"
10
+ @click="setDepartment({ mode: 'add' })"
11
+ size="large"
12
+ >
13
+ Add {{ formTitle }}
14
+ </v-btn>
15
+ </v-row>
16
+ </v-col>
17
+
18
+ <v-col cols="12">
19
+ <v-card
20
+ width="100%"
21
+ variant="outlined"
22
+ border="thin"
23
+ rounded="lg"
24
+ :loading="loadingDepartments"
25
+ >
26
+ <v-toolbar density="compact" color="grey-lighten-4">
27
+ <template #prepend>
28
+ <v-btn
29
+ fab
30
+ icon
31
+ density="comfortable"
32
+ @click="refreshDepartments()"
33
+ >
34
+ <v-icon>mdi-refresh</v-icon>
35
+ </v-btn>
36
+ </template>
37
+
38
+ <template #append>
39
+ <v-row no-gutters justify="end" align="center">
40
+ <span class="mr-2 text-caption text-fontgray">
41
+ {{ pageRange }}
42
+ </span>
43
+ <local-pagination v-model="page" :length="pages" />
44
+ </v-row>
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 - (158px))"
58
+ >
59
+ <template #item.name="{ item }">
60
+ <span
61
+ :class="{
62
+ 'pl-6': item.parent && item.path.split('.').length === 2,
63
+ 'pl-10': item.parent && item.path.split('.').length === 3,
64
+ 'pl-16': item.parent && item.path.split('.').length >= 4,
65
+ }"
66
+ >
67
+ {{ item.name }}
68
+ </span>
69
+ </template>
70
+ </v-data-table>
71
+ </v-card>
72
+ </v-col>
73
+
74
+ <!-- dialogs -->
75
+ <v-dialog v-model="dialogAdd" width="450" persistent>
76
+ <CategoryForm
77
+ v-model="category"
78
+ mode="add"
79
+ :title="`Add ${formTitle}`"
80
+ :level="localProps.level"
81
+ :org="localProps.org"
82
+ @close="setDepartment({ mode: 'add', dialog: false })"
83
+ @submit="submitAdd()"
84
+ v-model:message="messageCategory"
85
+ />
86
+ </v-dialog>
87
+
88
+ <v-dialog v-model="dialogView" width="450" persistent>
89
+ <CategoryForm
90
+ v-model="category"
91
+ mode="view"
92
+ :title="`View ${formTitle} Details`"
93
+ :level="localProps.level"
94
+ :org="localProps.org"
95
+ @close="setDepartment({ mode: 'view', dialog: false })"
96
+ @edit="handleEdit()"
97
+ @delete="handleDelete()"
98
+ />
99
+ </v-dialog>
100
+
101
+ <v-dialog v-model="dialogEdit" width="450" persistent>
102
+ <CategoryForm
103
+ v-model="category"
104
+ mode="edit"
105
+ :title="`Edit ${formTitle} Details`"
106
+ :level="localProps.level"
107
+ :org="localProps.org"
108
+ @close="setDepartment({ mode: 'edit', dialog: false })"
109
+ @submit="submitEdit()"
110
+ v-model:message="messageCategory"
111
+ />
112
+ </v-dialog>
113
+
114
+ <v-dialog v-model="dialogDelete" width="450" persistent>
115
+ <ConfirmationPrompt
116
+ :title="`Delete ${formTitle}`"
117
+ action="Delete Category"
118
+ :content="`Are you sure you want to delete this ${formTitle.toLowerCase()}? This action cannot be undone.`"
119
+ @cancel="dialogDelete = false"
120
+ @confirm="submitDelete()"
121
+ :message="messageCategory"
122
+ :loading="loadingSubmit"
123
+ :disabled="loadingSubmit"
124
+ />
125
+ </v-dialog>
126
+ </v-row>
127
+ </v-col>
128
+ </template>
129
+
130
+ <script setup lang="ts">
131
+ const localProps = defineProps({
132
+ org: {
133
+ type: String,
134
+ default: "",
135
+ },
136
+ level: {
137
+ type: String,
138
+ default: "department",
139
+ },
140
+ });
141
+
142
+ const formTitle = computed(() => {
143
+ switch (localProps.level) {
144
+ case "department":
145
+ return "Department";
146
+ case "category":
147
+ return "Category";
148
+ case "subcategory":
149
+ return "Subcategory";
150
+ default:
151
+ return "Category";
152
+ }
153
+ });
154
+
155
+ const headers = [{ title: "Name", value: "displayName" }];
156
+
157
+ const page = ref(1);
158
+ const pages = ref(0);
159
+ const pageRange = ref("-- - -- of --");
160
+
161
+ const items = ref<Array<Record<string, any>>>([]);
162
+
163
+ const { getAll, category, add, updateById, deleteById } = useCategory();
164
+
165
+ const {
166
+ data: dataDepartments,
167
+ refresh: refreshDepartments,
168
+ status: statusDepartments,
169
+ } = await useLazyAsyncData(
170
+ `get-all-from-category-registry-${localProps.org}-${localProps.level}-${page.value}`,
171
+ () =>
172
+ getAll({
173
+ page: page.value,
174
+ limit: 20,
175
+ org: localProps.org,
176
+ level: localProps.level,
177
+ })
178
+ );
179
+
180
+ const loadingDepartments = computed(
181
+ () => statusDepartments.value === "pending"
182
+ );
183
+
184
+ watchEffect(() => {
185
+ if (dataDepartments.value) {
186
+ items.value = dataDepartments.value.items;
187
+ pages.value = dataDepartments.value.pages;
188
+ pageRange.value = dataDepartments.value.pageRange;
189
+ }
190
+ });
191
+
192
+ watch(page, () => {
193
+ refreshDepartments();
194
+ });
195
+
196
+ // Dialogs
197
+ const dialogAdd = ref(false);
198
+ const dialogView = ref(false);
199
+ const dialogEdit = ref(false);
200
+ const dialogDelete = ref(false);
201
+
202
+ function setDepartment({
203
+ data = category.value,
204
+ mode = "",
205
+ dialog = true,
206
+ } = {}) {
207
+ Object.assign(category.value, JSON.parse(JSON.stringify(data)));
208
+
209
+ switch (mode) {
210
+ case "add":
211
+ dialogAdd.value = dialog;
212
+ break;
213
+ case "view":
214
+ dialogView.value = dialog;
215
+ break;
216
+ case "edit":
217
+ dialogEdit.value = dialog;
218
+ break;
219
+ }
220
+ }
221
+
222
+ const loadingSubmit = ref(false);
223
+ const messageCategory = ref("");
224
+
225
+ async function submitAdd() {
226
+ loadingSubmit.value = true;
227
+ try {
228
+ const payload: Pick<TCategory, "displayName" | "parentId"> = {
229
+ displayName: category.value.displayName,
230
+ };
231
+ if (category.value.parentId) {
232
+ payload.parentId = category.value.parentId;
233
+ }
234
+ await add(payload, localProps.org);
235
+ await refreshDepartments();
236
+ setDepartment({ mode: "add", dialog: false });
237
+ } catch (error: any) {
238
+ messageCategory.value =
239
+ error.response?._data?.message ||
240
+ `An error occurred while adding the ${formTitle.value.toLowerCase()}.`;
241
+ } finally {
242
+ loadingSubmit.value = false;
243
+ }
244
+ }
245
+
246
+ function tableRowClickHandler(_: any, data: any) {
247
+ setDepartment({ mode: "view", data: data.item });
248
+ }
249
+
250
+ function handleEdit() {
251
+ dialogView.value = false;
252
+ dialogEdit.value = true;
253
+ }
254
+
255
+ async function submitEdit() {
256
+ loadingSubmit.value = true;
257
+ try {
258
+ await updateById(category.value._id ?? "", {
259
+ displayName: category.value.displayName,
260
+ });
261
+ await refreshDepartments();
262
+ setDepartment({ mode: "edit", dialog: false });
263
+ } catch (error: any) {
264
+ messageCategory.value =
265
+ error.response?._data?.message ||
266
+ "An error occurred while editing the category.";
267
+ } finally {
268
+ loadingSubmit.value = false;
269
+ }
270
+ }
271
+
272
+ function handleDelete() {
273
+ dialogView.value = false;
274
+ dialogDelete.value = true;
275
+ }
276
+
277
+ async function submitDelete() {
278
+ loadingSubmit.value = true;
279
+ try {
280
+ await deleteById(category.value._id ?? "");
281
+ await refreshDepartments();
282
+ setDepartment({ mode: "view", dialog: false });
283
+ dialogDelete.value = false;
284
+ } catch (error: any) {
285
+ messageCategory.value =
286
+ error.response?._data?.message ||
287
+ "An error occurred while deleting the category.";
288
+ } finally {
289
+ loadingSubmit.value = false;
290
+ }
291
+ }
292
+ </script>
@@ -12,11 +12,18 @@
12
12
  </template>
13
13
 
14
14
  <script setup lang="ts">
15
- const model = defineModel<string>({ default: "" });
15
+ const props = defineProps({
16
+ case: {
17
+ type: String as PropType<"uppercase" | "lowercase">,
18
+ default: "uppercase",
19
+ },
20
+ });
16
21
 
17
- const { toSnakeUpperCase } = useUtils();
22
+ const model = defineModel<string>({ default: "" });
18
23
 
19
24
  function onInput(val: string) {
20
- model.value = toSnakeUpperCase(val);
25
+ const snake = val.replace(/\s+/g, "_");
26
+ model.value =
27
+ props.case === "lowercase" ? snake.toLowerCase() : snake.toUpperCase();
21
28
  }
22
29
  </script>
@@ -0,0 +1,186 @@
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">{{ localProps.title }}</span>
6
+ </v-row>
7
+ </v-toolbar>
8
+
9
+ <v-card-text style="max-height: 100vh; overflow-y: auto">
10
+ <v-row no-gutters>
11
+ <v-col cols="12" class="mb-2">
12
+ <v-row no-gutters>
13
+ <InputLabel title="Name" :required="isMutable" />
14
+ <v-col cols="12">
15
+ <v-text-field
16
+ v-model="tag.name"
17
+ :rules="isMutable ? [requiredRule] : []"
18
+ :readonly="!isMutable"
19
+ ></v-text-field>
20
+ </v-col>
21
+ </v-row>
22
+ </v-col>
23
+
24
+ <v-col cols="12" class="mb-2">
25
+ <v-row no-gutters>
26
+ <InputLabel title="Type" :required="isMutable" />
27
+ <v-col cols="12">
28
+ <v-select
29
+ v-model="tag.type"
30
+ :items="['public', 'private']"
31
+ :readonly="!isMutable"
32
+ ></v-select>
33
+ </v-col>
34
+ </v-row>
35
+ </v-col>
36
+
37
+ <v-col cols="12" class="mb-2">
38
+ <v-row no-gutters>
39
+ <InputLabel title="Category Path" />
40
+ <v-col cols="12">
41
+ <v-text-field
42
+ v-model="tag.categoryPath"
43
+ :readonly="!isMutable"
44
+ ></v-text-field>
45
+ </v-col>
46
+ </v-row>
47
+ </v-col>
48
+
49
+ <v-col v-if="localProps.mode !== 'add'" cols="12" class="mb-2">
50
+ <v-row no-gutters>
51
+ <InputLabel title="Status" />
52
+ <v-col cols="12">
53
+ <v-select
54
+ v-model="tag.status"
55
+ :items="['active', 'pending']"
56
+ :readonly="!isMutable"
57
+ ></v-select>
58
+ </v-col>
59
+ </v-row>
60
+ </v-col>
61
+ </v-row>
62
+
63
+ <v-alert
64
+ v-if="message"
65
+ type="error"
66
+ variant="flat"
67
+ closable
68
+ position="absolute"
69
+ location="bottom"
70
+ style="bottom: 48px"
71
+ @click:close="message = ''"
72
+ width="100%"
73
+ tile
74
+ class="text-caption"
75
+ >
76
+ {{ message }}
77
+ </v-alert>
78
+ </v-card-text>
79
+
80
+ <v-toolbar density="compact">
81
+ <v-row no-gutters>
82
+ <v-col cols="6">
83
+ <v-btn
84
+ tile
85
+ block
86
+ variant="text"
87
+ class="text-none"
88
+ size="48"
89
+ @click="emits('close')"
90
+ :disabled="localProps.loading"
91
+ >
92
+ {{ isMutable ? "Cancel" : "Close" }}
93
+ </v-btn>
94
+ </v-col>
95
+
96
+ <v-col v-if="localProps.mode === 'view'" cols="6">
97
+ <v-menu>
98
+ <template #activator="{ props }">
99
+ <v-btn
100
+ block
101
+ variant="flat"
102
+ color="black"
103
+ class="text-none"
104
+ height="48"
105
+ v-bind="props"
106
+ tile
107
+ >
108
+ More actions
109
+ </v-btn>
110
+ </template>
111
+
112
+ <v-list class="pa-0">
113
+ <v-list-item @click="emits('edit')">
114
+ <v-list-item-title class="text-subtitle-2">
115
+ Edit
116
+ </v-list-item-title>
117
+ </v-list-item>
118
+
119
+ <v-list-item @click="emits('delete')" class="text-red">
120
+ <v-list-item-title class="text-subtitle-2">
121
+ Delete
122
+ </v-list-item-title>
123
+ </v-list-item>
124
+ </v-list>
125
+ </v-menu>
126
+ </v-col>
127
+
128
+ <v-col v-if="isMutable" cols="6">
129
+ <v-btn
130
+ tile
131
+ block
132
+ variant="flat"
133
+ color="black"
134
+ class="text-none"
135
+ size="48"
136
+ @click="emits('submit')"
137
+ :loading="localProps.loading"
138
+ >
139
+ {{ submitTitle }}
140
+ </v-btn>
141
+ </v-col>
142
+ </v-row>
143
+ </v-toolbar>
144
+ </v-card>
145
+ </template>
146
+
147
+ <script setup lang="ts">
148
+ const localProps = defineProps({
149
+ title: {
150
+ type: String,
151
+ default: "Tag Form",
152
+ },
153
+ mode: {
154
+ type: String,
155
+ default: "add",
156
+ },
157
+ loading: {
158
+ type: Boolean,
159
+ default: false,
160
+ },
161
+ });
162
+
163
+ const isMutable = computed(() => ["add", "edit"].includes(localProps.mode));
164
+
165
+ const submitTitle = computed(() => {
166
+ switch (localProps.mode) {
167
+ case "add":
168
+ return "Submit";
169
+ case "edit":
170
+ return "Save changes";
171
+ default:
172
+ return "";
173
+ }
174
+ });
175
+
176
+ const emits = defineEmits(["edit", "delete", "submit", "cancel", "close"]);
177
+
178
+ const tag = defineModel({
179
+ type: Object as PropType<TTag>,
180
+ default: () => useTag().tag.value,
181
+ });
182
+
183
+ const message = defineModel("message", { type: String, default: "" });
184
+
185
+ const { requiredRule } = useUtils();
186
+ </script>
@@ -0,0 +1,88 @@
1
+ export default function useCategory() {
2
+ const category = ref<TCategory>({
3
+ _id: "",
4
+ level: "department",
5
+ name: "",
6
+ displayName: "",
7
+ parentId: "",
8
+ path: "",
9
+ isActive: true,
10
+ });
11
+
12
+ function add(
13
+ value: Pick<TCategory, "displayName" | "parentId">,
14
+ org?: string
15
+ ) {
16
+ return useNuxtApp().$api<Record<string, any>>(
17
+ org
18
+ ? `/api/asset/item/categories/org/${org}`
19
+ : `/api/asset/item/categories`,
20
+ {
21
+ method: "POST",
22
+ body: value,
23
+ }
24
+ );
25
+ }
26
+
27
+ function getAll({
28
+ org = "",
29
+ parent = "",
30
+ level = "",
31
+ search = "",
32
+ limit = 20,
33
+ page = 1,
34
+ } = {}) {
35
+ return useNuxtApp().$api<Record<string, any>>(
36
+ org
37
+ ? `/api/asset/item/categories/org/${org}`
38
+ : `/api/asset/item/categories`,
39
+ {
40
+ method: "GET",
41
+ query: {
42
+ parent,
43
+ level,
44
+ search,
45
+ limit,
46
+ page,
47
+ },
48
+ }
49
+ );
50
+ }
51
+
52
+ function getById(id: string) {
53
+ return useNuxtApp().$api<TCategory>(`/api/asset/item/categories/id/${id}`, {
54
+ method: "GET",
55
+ });
56
+ }
57
+
58
+ function updateById(
59
+ id: string,
60
+ options: Pick<TCategory, "displayName"> & { isActive?: boolean }
61
+ ) {
62
+ return useNuxtApp().$api<Record<string, any>>(
63
+ `/api/asset/item/categories/id/${id}`,
64
+ {
65
+ method: "PATCH",
66
+ body: options,
67
+ }
68
+ );
69
+ }
70
+
71
+ function deleteById(id: string) {
72
+ return useNuxtApp().$api<Record<string, any>>(
73
+ `/api/asset/item/categories/id/${id}`,
74
+ {
75
+ method: "DELETE",
76
+ }
77
+ );
78
+ }
79
+
80
+ return {
81
+ category,
82
+ add,
83
+ getAll,
84
+ getById,
85
+ updateById,
86
+ deleteById,
87
+ };
88
+ }
@@ -0,0 +1,78 @@
1
+ export default function useTag() {
2
+ const tag = ref<TTag>({
3
+ _id: "",
4
+ name: "",
5
+ normalizedName: "",
6
+ type: "public",
7
+ orgId: "",
8
+ categoryPath: "",
9
+ status: "pending",
10
+ usageCount: 0,
11
+ });
12
+
13
+ function add(
14
+ value: Pick<TTag, "name" | "type"> &
15
+ Partial<Pick<TTag, "orgId" | "categoryPath">>
16
+ ) {
17
+ return useNuxtApp().$api<Record<string, any>>(`/api/tags`, {
18
+ method: "POST",
19
+ body: value,
20
+ });
21
+ }
22
+
23
+ function getAll(options?: {
24
+ search?: string;
25
+ page?: number;
26
+ limit?: number;
27
+ type?: string;
28
+ orgId?: string;
29
+ status?: string;
30
+ categoryPath?: string;
31
+ }) {
32
+ return useNuxtApp().$api<Record<string, any>>(`/api/tags`, {
33
+ method: "GET",
34
+ query: {
35
+ search: options?.search ?? "",
36
+ page: options?.page ?? 1,
37
+ limit: options?.limit ?? 10,
38
+ type: options?.type ?? "",
39
+ orgId: options?.orgId ?? "",
40
+ status: options?.status ?? "",
41
+ categoryPath: options?.categoryPath ?? "",
42
+ },
43
+ });
44
+ }
45
+
46
+ function getById(id: string) {
47
+ return useNuxtApp().$api<Record<string, any>>(`/api/tags/id/${id}`, {
48
+ method: "GET",
49
+ });
50
+ }
51
+
52
+ function updateById(
53
+ id: string,
54
+ options: Partial<
55
+ Pick<TTag, "name" | "type" | "orgId" | "categoryPath" | "status">
56
+ >
57
+ ) {
58
+ return useNuxtApp().$api<Record<string, any>>(`/api/tags/id/${id}`, {
59
+ method: "PATCH",
60
+ body: options,
61
+ });
62
+ }
63
+
64
+ function deleteById(id: string) {
65
+ return useNuxtApp().$api<Record<string, any>>(`/api/tags/id/${id}`, {
66
+ method: "DELETE",
67
+ });
68
+ }
69
+
70
+ return {
71
+ tag,
72
+ add,
73
+ getAll,
74
+ getById,
75
+ updateById,
76
+ deleteById,
77
+ };
78
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@goweekdays/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.6.9",
5
+ "version": "1.6.10",
6
6
  "main": "./nuxt.config.ts",
7
7
  "publishConfig": {
8
8
  "access": "public"
@@ -0,0 +1,33 @@
1
+ declare type TAssetItemCategory =
2
+ | "facility"
3
+ | "equipment"
4
+ | "vehicle"
5
+ | "tool"
6
+ | "supply";
7
+
8
+ declare type TAssetItemPurpose = "internal" | "for-sale" | "for-rent";
9
+
10
+ declare type TAssetItemTrackMode = "individual" | "quantity";
11
+
12
+ declare type TAssetItemStatus = "active" | "archived";
13
+
14
+ declare type TAssetItem = {
15
+ _id?: ObjectId;
16
+ name: string;
17
+ itemRef?: string;
18
+ category: TAssetItemCategory;
19
+ tags: string[];
20
+ purpose: TAssetItemPurpose;
21
+ trackMode: TAssetItemTrackMode;
22
+ sku?: string;
23
+ price?: number;
24
+ unit: string;
25
+ quantityOnHand: number;
26
+ reserved: number;
27
+ locationId?: string;
28
+ assignedTo?: string;
29
+ status?: TAssetItemStatus;
30
+ createdAt?: string;
31
+ updatedAt?: string;
32
+ deletedAt?: string;
33
+ };
@@ -0,0 +1,13 @@
1
+ declare type TCategoryLevel = "department" | "category" | "subcategory";
2
+
3
+ declare type TCategory = {
4
+ _id?: string;
5
+ level: TCategoryLevel;
6
+ name: string;
7
+ displayName: string;
8
+ parentId?: string;
9
+ path: string;
10
+ isActive: boolean;
11
+ createdAt?: Date | string;
12
+ updatedAt?: Date | string;
13
+ };
package/types/tag.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ declare type TTag = {
2
+ _id?: string;
3
+ name: string;
4
+ normalizedName: string;
5
+ type: "public" | "private";
6
+ orgId?: string;
7
+ categoryPath?: string;
8
+ status?: "active" | "pending";
9
+ usageCount?: number;
10
+ createdAt?: Date | string;
11
+ updatedAt?: Date | string;
12
+ };