@iservice365/layer-common 1.5.7 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9cbea14: Publish Changes in Components (CKEditor)
8
+
3
9
  ## 1.5.7
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12" class="mb-2">
4
+ <v-row no-gutters align="center" justify="space-between">
5
+ <v-btn
6
+ class="text-none"
7
+ rounded="pill"
8
+ variant="tonal"
9
+ size="large"
10
+ v-if="canCreate && canCreateAccessCard"
11
+ >
12
+ Add Access Card
13
+ </v-btn>
14
+
15
+ <v-text-field
16
+ v-model="searchText"
17
+ placeholder="Search Unit..."
18
+ variant="outlined"
19
+ density="comfortable"
20
+ clearable
21
+ hide-details
22
+ class="ml-2"
23
+ style="max-width: 250px"
24
+ />
25
+ </v-row>
26
+ </v-col>
27
+ <v-col cols="12">
28
+ <v-card
29
+ width="100%"
30
+ variant="outlined"
31
+ border="thin"
32
+ rounded="lg"
33
+ :loading="loading"
34
+ >
35
+ <v-toolbar density="compact" color="grey-lighten-4">
36
+ <template #prepend>
37
+ <v-btn fab icon density="comfortable">
38
+ <v-icon>mdi-refresh</v-icon>
39
+ </v-btn>
40
+ </template>
41
+
42
+ <template #append>
43
+ <v-row no-gutters justify="end" align="center">
44
+ <span class="mr-2 text-caption text-fontgray">
45
+ {{ pageRange }}
46
+ </span>
47
+ <local-pagination v-model="page" :length="pages" />
48
+ </v-row>
49
+ </template>
50
+ </v-toolbar>
51
+ <v-data-table
52
+ :headers="headers"
53
+ :items="items"
54
+ item-value="_id"
55
+ items-per-page="10"
56
+ fixed-header
57
+ hide-default-footer
58
+ hide-default-header
59
+ @click:row="(_: any, data: any) => $emit('row-click', data)"
60
+ style="max-height: calc(100vh - (200px))"
61
+ />
62
+ </v-card>
63
+ </v-col>
64
+
65
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
66
+ </v-row>
67
+ </template>
68
+ <script setup lang="ts">
69
+ definePageMeta({
70
+ middleware: ["01-auth", "02-org"],
71
+ memberOnly: true,
72
+ });
73
+ const props = defineProps({
74
+ headers: {
75
+ type: Array as PropType<Array<Record<string, any>>>,
76
+ default: () => [
77
+ {
78
+ title: "Card",
79
+ value: "card",
80
+ },
81
+ {
82
+ title: "Unit",
83
+ value: "unit",
84
+ },
85
+ {
86
+ title: "Assign",
87
+ value: "assign",
88
+ },
89
+ { title: "Action", value: "action-table" },
90
+ ],
91
+ },
92
+ canCreate: {
93
+ type: Boolean,
94
+ default: true,
95
+ },
96
+ canUpdate: {
97
+ type: Boolean,
98
+ default: true,
99
+ },
100
+ canDelete: {
101
+ type: Boolean,
102
+ default: true,
103
+ },
104
+ canCreateAccessCard: {
105
+ type: Boolean,
106
+ default: true,
107
+ },
108
+ canUpdateAccessCard: {
109
+ type: Boolean,
110
+ default: true,
111
+ },
112
+ canDeleteAccessCard: {
113
+ type: Boolean,
114
+ default: true,
115
+ },
116
+ });
117
+
118
+ const { headerSearch } = useLocal();
119
+ const page = ref(1);
120
+ const pages = ref(0);
121
+ const pageRange = ref("-- - -- of --");
122
+
123
+ const message = ref("");
124
+ const messageSnackbar = ref(false);
125
+ const messageColor = ref("");
126
+
127
+ const items = ref<Array<Record<string, any>>>([]);
128
+ const createDialog = ref(false);
129
+ const editDialog = ref(false);
130
+ const previewDialog = ref(false);
131
+ const deleteLoading = ref(false);
132
+ const confirmDialog = ref(false);
133
+ const searchText = ref("");
134
+
135
+ const loading = ref(false);
136
+ </script>
@@ -15,10 +15,19 @@
15
15
  v-model="document.attachment"
16
16
  :multiple="false"
17
17
  :max-length="10"
18
- title="Upload Images"
18
+ title="Upload PDF Files"
19
+ accept=".pdf, .doc, .docx, .xls, .xlsx, .txt"
19
20
  />
20
21
  </v-col>
21
22
 
23
+ <v-col cols="12" class="px-6">
24
+ <InputLabel class="text-capitalize" title="Document Name" />
25
+ <v-text-field
26
+ v-model.trim="document.name"
27
+ density="comfortable"
28
+ ></v-text-field>
29
+ </v-col>
30
+
22
31
  <v-col cols="12">
23
32
  <v-row no-gutters>
24
33
  <v-col cols="12" class="text-center">
@@ -57,7 +66,12 @@
57
66
  color="black"
58
67
  class="text-none"
59
68
  size="48"
60
- :disabled="!validForm || disable"
69
+ :disabled="
70
+ !validForm ||
71
+ disable ||
72
+ document.attachment.length == 0 ||
73
+ document.name == ''
74
+ "
61
75
  @click="submit"
62
76
  :loading="disable"
63
77
  >
@@ -78,17 +92,42 @@ const prop = defineProps({
78
92
  type: Object as PropType<TDocument>,
79
93
  default: () => ({
80
94
  name: "",
81
- attachment: "",
95
+ attachment: [],
82
96
  }),
83
97
  },
84
98
  });
85
99
 
86
100
  const emit = defineEmits(["cancel", "success"]);
87
101
 
102
+ const { add, updateById } = useDocument();
103
+ const { getFileById } = useFile();
104
+
88
105
  const validForm = ref(false);
89
106
  const disable = ref(false);
90
107
  const message = ref("");
91
108
 
109
+ const document = ref<Record<string, any>>({
110
+ name: "",
111
+ attachment: [],
112
+ });
113
+
114
+ if (prop.mode === "edit") {
115
+ document.value.name = prop.document.name;
116
+ document.value.attachment = [prop.document.attachment];
117
+ }
118
+
119
+ watch(
120
+ () => document.value.attachment,
121
+ async (newVal) => {
122
+ if (newVal) {
123
+ const fileData = await getFileById(newVal);
124
+ if (fileData) {
125
+ document.value.name = fileData.data.name;
126
+ }
127
+ }
128
+ }
129
+ );
130
+
92
131
  function cancel() {
93
132
  // createMore.value = false;
94
133
  message.value = "";
@@ -97,5 +136,30 @@ function cancel() {
97
136
 
98
137
  async function submit() {
99
138
  disable.value = true;
139
+ try {
140
+ if (prop.mode === "add") {
141
+ const payload = {
142
+ name: document.value.name,
143
+ attachment: document.value.attachment,
144
+ status: "active",
145
+ };
146
+ await add(payload);
147
+ }
148
+
149
+ if (prop.mode === "edit") {
150
+ const payload = {
151
+ name: document.value.name,
152
+ attachment: document.value.attachment,
153
+ }
154
+ await updateById(prop.document._id ?? "", payload);
155
+ }
156
+
157
+ emit("success");
158
+ } catch (error: any) {
159
+ message.value =
160
+ error.response?._data?.message || "Failed to create document";
161
+ } finally {
162
+ disable.value = false;
163
+ }
100
164
  }
101
165
  </script>
@@ -24,7 +24,7 @@
24
24
  >
25
25
  <v-toolbar density="compact" color="grey-lighten-4">
26
26
  <template #prepend>
27
- <v-btn fab icon density="comfortable" @click="">
27
+ <v-btn fab icon density="comfortable" @click="getDocuments">
28
28
  <v-icon>mdi-refresh</v-icon>
29
29
  </v-btn>
30
30
  </template>
@@ -37,7 +37,7 @@
37
37
  <local-pagination
38
38
  v-model="page"
39
39
  :length="pages"
40
- @update:value=""
40
+ @update:value="getDocuments"
41
41
  />
42
42
  </v-row>
43
43
  </template>
@@ -60,6 +60,154 @@
60
60
  <v-dialog v-model="createDialog" width="450" persistent>
61
61
  <DocumentForm @cancel="createDialog = false" @success="successCreate()" />
62
62
  </v-dialog>
63
+
64
+ <!-- Edit Dialog -->
65
+ <v-dialog v-model="editDialog" width="450" persistent>
66
+ <DocumentForm
67
+ mode="edit"
68
+ @cancel="editDialog = false"
69
+ @success="successUpdate()"
70
+ :document="selectedDocument"
71
+ />
72
+ </v-dialog>
73
+
74
+ <!-- Preview Dialog -->
75
+ <v-dialog v-model="previewDialog" width="450" persistent>
76
+ <v-card width="100%">
77
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
78
+ <v-row no-gutters class="mb-4">
79
+ <v-col cols="12">
80
+ <strong>Name:</strong> {{ selectedDocument?.name ?? "N/A" }}
81
+ </v-col>
82
+
83
+ <v-col cols="12">
84
+ <strong>Document: </strong>
85
+ <NuxtLink
86
+ :to="getFileUrl(selectedDocument.attachment)"
87
+ external
88
+ target="_blank"
89
+ >
90
+ View Document
91
+ </NuxtLink>
92
+ </v-col>
93
+ </v-row>
94
+ </v-card-text>
95
+ <v-toolbar class="pa-0" density="compact">
96
+ <v-row no-gutters>
97
+ <v-col cols="6" class="pa-0">
98
+ <v-btn
99
+ block
100
+ variant="text"
101
+ class="text-none"
102
+ size="large"
103
+ @click="previewDialog = false"
104
+ height="48"
105
+ >
106
+ Close
107
+ </v-btn>
108
+ </v-col>
109
+ <v-col cols="6" class="pa-0" v-if="canUpdate">
110
+ <v-menu>
111
+ <template #activator="{ props }">
112
+ <v-btn
113
+ block
114
+ variant="flat"
115
+ color="black"
116
+ class="text-none"
117
+ height="48"
118
+ v-bind="props"
119
+ tile
120
+ :disabled="!canUpdateDocument && !canDeleteDocument"
121
+ >
122
+ More actions
123
+ </v-btn>
124
+ </template>
125
+ <v-list class="pa-0">
126
+ <v-list-item
127
+ v-if="canUpdateDocument"
128
+ @click="openEditDialog()"
129
+ >
130
+ <v-list-item-title class="text-subtitle-2">
131
+ Edit Document
132
+ </v-list-item-title>
133
+ </v-list-item>
134
+
135
+ <v-list-item
136
+ v-if="canDeleteDocument"
137
+ @click="openDeleteDialog()"
138
+ class="text-red"
139
+ >
140
+ <v-list-item-title class="text-subtitle-2">
141
+ Delete Document
142
+ </v-list-item-title>
143
+ </v-list-item>
144
+ </v-list>
145
+ </v-menu>
146
+ </v-col>
147
+ </v-row>
148
+ </v-toolbar>
149
+ </v-card>
150
+ </v-dialog>
151
+
152
+ <!-- Delete Dialog -->
153
+ <v-dialog
154
+ v-model="confirmDialog"
155
+ :loading="deleteLoading"
156
+ width="450"
157
+ persistent
158
+ >
159
+ <v-card width="100%">
160
+ <v-toolbar density="compact" class="pl-4">
161
+ <span class="font-weight-medium text-h5">Delete Document</span>
162
+ </v-toolbar>
163
+ <v-card-text>
164
+ <p class="text-subtitle-2 text-center">
165
+ Are you sure you want to delete this document? This action cannot be
166
+ undone.
167
+ </p>
168
+
169
+ <v-row v-if="message" no-gutters justify="center" class="mt-4">
170
+ <span class="text-caption text-error text-center">
171
+ {{ message }}
172
+ </span>
173
+ </v-row>
174
+ </v-card-text>
175
+
176
+ <v-toolbar density="compact">
177
+ <v-row no-gutters>
178
+ <v-col cols="6">
179
+ <v-btn
180
+ tile
181
+ block
182
+ size="48"
183
+ variant="text"
184
+ class="text-none"
185
+ @click="confirmDialog = false"
186
+ :disabled="deleteLoading"
187
+ >
188
+ Close
189
+ </v-btn>
190
+ </v-col>
191
+ <v-col cols="6">
192
+ <v-btn
193
+ tile
194
+ block
195
+ size="48"
196
+ color="black"
197
+ variant="flat"
198
+ class="text-none"
199
+ @click="handleDeleteDocument"
200
+ :loading="deleteLoading"
201
+ >
202
+ Delete Document
203
+ </v-btn>
204
+ </v-col>
205
+ </v-row>
206
+ </v-toolbar>
207
+ </v-card>
208
+ </v-dialog>
209
+
210
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
63
211
  </v-row>
64
212
  </template>
65
213
  <script setup lang="ts">
@@ -105,7 +253,8 @@ const props = defineProps({
105
253
  });
106
254
 
107
255
  const { headerSearch } = useLocal();
108
- const { getAll: _getAllDocuments } = useDocument();
256
+ const { getAll: _getAllDocuments, deleteById } = useDocument();
257
+ const { getFileUrl } = useFile();
109
258
 
110
259
  const page = ref(1);
111
260
  const pages = ref(0);
@@ -149,8 +298,11 @@ const previewDialog = ref(false);
149
298
  const selectedDocument = ref<TDocument>({
150
299
  _id: "",
151
300
  name: "",
152
- attachment: [],
301
+ attachment: "",
153
302
  });
303
+ const deleteLoading = ref(false);
304
+ const confirmDialog = ref(false);
305
+ const selectedDocumentId = ref<string | null>(null);
154
306
 
155
307
  function tableRowClickHandler(_: any, data: any) {
156
308
  selectedDocument.value = data.item as TDocument;
@@ -184,4 +336,36 @@ function successCreate() {
184
336
  getDocuments();
185
337
  showMessage("Document created successfully!", "success");
186
338
  }
339
+
340
+ function openDeleteDialog() {
341
+ confirmDialog.value = true;
342
+ message.value = "";
343
+ }
344
+
345
+ function openEditDialog() {
346
+ editDialog.value = true;
347
+ }
348
+
349
+ async function handleDeleteDocument() {
350
+ deleteLoading.value = true;
351
+ try {
352
+ await deleteById(selectedDocument.value._id ?? "");
353
+ await getDocuments();
354
+ selectedDocumentId.value = null;
355
+ confirmDialog.value = false;
356
+ previewDialog.value = false;
357
+ } catch (error: any) {
358
+ message.value =
359
+ error?.response?._data?.message || "Failed to delete document";
360
+ } finally {
361
+ deleteLoading.value = false;
362
+ }
363
+ }
364
+
365
+ function successUpdate() {
366
+ editDialog.value = false;
367
+ previewDialog.value = false;
368
+ getDocuments();
369
+ showMessage("Document updated successfully!", "success");
370
+ };
187
371
  </script>
@@ -1,23 +1,30 @@
1
1
  <template>
2
- <ClientOnly>
3
- <v-row no-gutters class="ckeditor-container w-100">
4
- <ckeditor v-model="data" :editor="ClassicEditor" :config="config" />
5
- </v-row>
6
- </ClientOnly>
2
+ <ClientOnly>
3
+ <v-row no-gutters class="ckeditor-container w-100">
4
+ <ckeditor v-model="data" :editor="ClassicEditor" :config="config" @ready="onReady" :key="prop.readOnly" />
5
+ </v-row>
6
+ </ClientOnly>
7
7
  </template>
8
8
 
9
9
  <script setup>
10
+
11
+ const prop = defineProps({
12
+ readOnly: {
13
+ type: Boolean,
14
+ required: false
15
+ }
16
+ })
10
17
  import {
11
- ClassicEditor,
12
- Essentials,
13
- Paragraph,
14
- Bold,
15
- Italic,
16
- Heading,
17
- List,
18
- Link,
19
- Alignment,
20
- Font
18
+ ClassicEditor,
19
+ Essentials,
20
+ Paragraph,
21
+ Bold,
22
+ Italic,
23
+ Heading,
24
+ List,
25
+ Link,
26
+ Alignment,
27
+ Font,
21
28
  } from 'ckeditor5'
22
29
  import { Ckeditor } from '@ckeditor/ckeditor5-vue'
23
30
  import 'ckeditor5/ckeditor5.css';
@@ -26,17 +33,45 @@ import 'ckeditor5/ckeditor5.css'
26
33
 
27
34
  const data = defineModel({ required: true, default: "" })
28
35
 
36
+ const editorInstance = ref(null);
37
+
29
38
  const config = computed(() => ({
30
- licenseKey: 'GPL',
31
- plugins: [Essentials, Paragraph, Bold, Italic, Heading, List, Alignment, Font],
32
- toolbar: [
33
- 'undo', 'redo',
34
- '|', 'bold', 'italic',
35
- '|', 'fontSize', 'fontColor', 'fontBackgroundColor',
36
- '|', 'bulletedList', 'numberedList',
37
- '|', 'heading', '|', 'alignment:left', 'alignment:center', 'alignment:right', 'alignment:justify',
38
- ]
39
+ licenseKey: 'GPL',
40
+ plugins: [Essentials, Paragraph, Bold, Italic, Heading, List, Alignment, Font],
41
+ toolbar: [
42
+ 'undo', 'redo',
43
+ '|', 'bold', 'italic',
44
+ '|', 'fontSize', 'fontColor', 'fontBackgroundColor',
45
+ '|', 'bulletedList', 'numberedList',
46
+ '|', 'heading', '|', 'alignment:left', 'alignment:center', 'alignment:right', 'alignment:justify',
47
+ ],
39
48
  }))
49
+
50
+
51
+ function onReady(editor) {
52
+ editorInstance.value = editor;
53
+ toggleReadOnly(prop.readOnly);
54
+ }
55
+
56
+ function toggleReadOnly(isReadOnly) {
57
+ if (!editorInstance.value) return;
58
+
59
+
60
+ if (isReadOnly) {
61
+ editorInstance.value.enableReadOnlyMode("view-mode-lock");
62
+ } else {
63
+ editorInstance.value.disableReadOnlyMode("view-mode-lock");
64
+ }
65
+ }
66
+
67
+ watch(
68
+ () => prop.readOnly,
69
+ (newVal) => {
70
+ toggleReadOnly(newVal);
71
+ }
72
+ );
73
+
74
+
40
75
  </script>
41
76
 
42
77
  <style scoped>
@@ -58,4 +93,3 @@ const config = computed(() => ({
58
93
  box-sizing: border-box;
59
94
  }
60
95
  </style>
61
-
@@ -21,6 +21,7 @@
21
21
  <script setup lang="ts">
22
22
  import { ref, computed, type PropType } from 'vue'
23
23
  import type { ValidationRule } from 'vuetify/lib/types.mjs'
24
+ //@ts-ignore
24
25
  import phoneMasks from '~/utils/phoneMasks'
25
26
 
26
27
  const props = defineProps({
@@ -6,22 +6,42 @@ export default function useDocument() {
6
6
  status = "active",
7
7
  site = "",
8
8
  } = {}) {
9
- return useNuxtApp().$api<Record<string, any>>(
10
- `/api/documents`,
11
- {
12
- method: "GET",
13
- query: {
14
- page,
15
- search,
16
- limit,
17
- status,
18
- site,
19
- },
20
- }
21
- );
9
+ return useNuxtApp().$api<Record<string, any>>(`/api/documents`, {
10
+ method: "GET",
11
+ query: {
12
+ page,
13
+ search,
14
+ limit,
15
+ status,
16
+ site,
17
+ },
18
+ });
19
+ }
20
+
21
+ function add(value: any) {
22
+ return useNuxtApp().$api<Record<string, any>>("/api/documents", {
23
+ method: "POST",
24
+ body: value,
25
+ });
26
+ }
27
+
28
+ function deleteById(id: string) {
29
+ return useNuxtApp().$api(`/api/documents/${id}`, {
30
+ method: "DELETE",
31
+ });
32
+ }
33
+
34
+ function updateById(id: string, value: any) {
35
+ return useNuxtApp().$api(`/api/documents/${id}`, {
36
+ method: "PUT",
37
+ body: value,
38
+ });
22
39
  }
23
40
 
24
41
  return {
25
42
  getAll,
43
+ add,
44
+ deleteById,
45
+ updateById,
26
46
  };
27
47
  }
@@ -20,13 +20,13 @@ export default function useFile() {
20
20
  }
21
21
 
22
22
  async function urlToFile(url: string, filename: string): Promise<File> {
23
- const response = await fetch(url)
24
- const blob = await response.blob()
23
+ const response = await fetch(url);
24
+ const blob = await response.blob();
25
25
 
26
26
  // Try to extract MIME type if possible (e.g. image/png)
27
- const mimeType = blob.type || 'image/jpeg'
27
+ const mimeType = blob.type || "image/jpeg";
28
28
 
29
- return new File([blob], filename, { type: mimeType })
29
+ return new File([blob], filename, { type: mimeType });
30
30
  }
31
31
 
32
32
  function deleteFile(attachmentId: string) {
@@ -37,7 +37,12 @@ export default function useFile() {
37
37
  }
38
38
  );
39
39
  }
40
-
40
+
41
+ function getFileById(id: string) {
42
+ return useNuxtApp().$api<Record<string, any>>(`/api/files/id/${id}`, {
43
+ method: "GET",
44
+ });
45
+ }
41
46
 
42
47
  return {
43
48
  baseUrl,
@@ -45,5 +50,6 @@ export default function useFile() {
45
50
  deleteFile,
46
51
  urlToFile,
47
52
  getFileUrl,
53
+ getFileById,
48
54
  };
49
55
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@iservice365/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.5.7",
5
+ "version": "1.6.0",
6
6
  "main": "./nuxt.config.ts",
7
7
  "scripts": {
8
8
  "dev": "nuxi dev .playground",
@@ -1,5 +1,5 @@
1
1
  declare type TDocument = {
2
2
  _id?: string;
3
3
  name: string;
4
- attachment: string[];
4
+ attachment: string;
5
5
  };