@7365admin1/layer-common 3.0.3 → 3.0.6

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.
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
3
  <v-col cols="12" class="mb-2">
4
- <v-row no-gutters>
4
+ <v-row no-gutters style="gap: 8px">
5
5
  <v-btn
6
6
  class="text-none"
7
7
  rounded="pill"
@@ -32,6 +32,16 @@
32
32
  >
33
33
  <v-icon>mdi-refresh</v-icon>
34
34
  </v-btn>
35
+ <v-btn
36
+ fab
37
+ icon
38
+ density="comfortable"
39
+ :color="sortAlpha ? 'primary' : undefined"
40
+ :title="sortAlpha ? 'Sorted A–Z' : 'Sort A–Z'"
41
+ @click="sortAlpha = !sortAlpha"
42
+ >
43
+ <v-icon>mdi-sort-alphabetical-ascending</v-icon>
44
+ </v-btn>
35
45
  <v-text-field
36
46
  v-model="search"
37
47
  density="compact"
@@ -59,7 +69,7 @@
59
69
  </v-toolbar>
60
70
  <v-data-table
61
71
  :headers="headers"
62
- :items="items"
72
+ :items="sortedItems"
63
73
  item-value="_id"
64
74
  items-per-page="10"
65
75
  fixed-header
@@ -97,118 +107,55 @@
97
107
  </v-dialog>
98
108
 
99
109
  <!-- Preview Dialog -->
100
- <v-dialog v-model="previewDialog" width="450" persistent>
101
- <v-card width="100%">
102
- <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
103
- <v-row no-gutters class="mb-4">
104
- <v-col cols="12">
105
- <strong>Name:</strong> {{ selectedOnlineForm?.formType ?? "N/A" }}
106
- </v-col>
107
- <v-col v-if="selectedOnlineForm?.attachment" cols="12">
108
- <strong>Document: </strong>
109
- <NuxtLink
110
- :to="getFileUrl(selectedOnlineForm.attachment)"
111
- external
112
- target="_blank"
113
- >
114
- View Document
115
- </NuxtLink>
116
- </v-col>
117
- <v-col cols="12">
118
- <strong>Status:</strong> {{ selectedOnlineForm?.status ? selectedOnlineForm.status.charAt(0).toUpperCase() + selectedOnlineForm.status.slice(1) : "N/A" }}
119
- </v-col>
120
- <v-col cols="12">
121
- <strong>Inputs:</strong>
122
- <div
123
- v-if="selectedOnlineForm?.inputs?.length"
124
- class="d-flex flex-wrap gap-2 mt-1"
125
- >
126
- <v-chip
127
- v-for="(input, index) in selectedOnlineForm.inputs"
128
- :key="index"
129
- color="primary"
130
- variant="elevated"
131
- size="small"
132
- class="me-1 mb-2"
133
- >
134
- {{ input.label }}
135
- </v-chip>
136
- </div>
137
- </v-col>
138
- </v-row>
139
- </v-card-text>
140
- <v-toolbar class="pa-0" density="compact">
141
- <v-row no-gutters>
142
- <v-col cols="6" class="pa-0">
143
- <v-btn
144
- block
145
- variant="text"
146
- class="text-none"
147
- size="large"
148
- @click="previewDialog = false"
149
- height="48"
150
- >
151
- Close
152
- </v-btn>
153
- </v-col>
154
- <v-col cols="6" class="pa-0" v-if="canUpdate">
155
- <v-menu>
156
- <template #activator="{ props }">
157
- <v-btn
158
- block
159
- variant="flat"
160
- color="black"
161
- class="text-none"
162
- height="48"
163
- v-bind="props"
164
- tile
165
- :disabled="
166
- !canUpdateOnlineFormConfiguration &&
167
- !canDeleteOnlineFormConfiguration
168
- "
169
- >
170
- More actions
171
- </v-btn>
172
- </template>
173
- <v-list class="pa-0">
174
- <v-list-item @click="previewDialog = false; formRenderPreviewDialog = true">
175
- <v-list-item-title class="text-subtitle-2">
176
- Preview
177
- </v-list-item-title>
178
- </v-list-item>
179
-
180
- <v-list-item
181
- v-if="canUpdateOnlineFormConfiguration"
182
- @click="openEditDialog()"
183
- >
184
- <v-list-item-title class="text-subtitle-2">
185
- Edit
186
- </v-list-item-title>
187
- </v-list-item>
110
+ <v-dialog v-model="previewDialog" width="600" persistent scrollable>
111
+ <OnlineFormFill
112
+ v-if="selectedFormDefinition"
113
+ :definition="selectedFormDefinition"
114
+ :site-id="siteId"
115
+ :org-id="orgId"
116
+ readonly
117
+ @cancel="previewDialog = false"
118
+ >
119
+ <template #title-actions>
120
+ <v-btn
121
+ v-if="canUpdate && canUpdateOnlineFormConfiguration"
122
+ icon
123
+ size="small"
124
+ variant="text"
125
+ color="white"
126
+ @click="openEditDialog()"
127
+ >
128
+ <v-icon>mdi-pencil</v-icon>
129
+ </v-btn>
130
+ <v-btn
131
+ v-if="canDelete && canDeleteOnlineFormConfiguration"
132
+ icon
133
+ size="small"
134
+ variant="text"
135
+ color="error"
136
+ @click="openDeleteDialog()"
137
+ >
138
+ <v-icon>mdi-delete</v-icon>
139
+ </v-btn>
140
+ </template>
141
+ </OnlineFormFill>
188
142
 
189
- <v-list-item
190
- v-if="canDeleteOnlineFormConfiguration"
191
- class="text-red"
192
- @click="openDeleteDialog()"
193
- >
194
- <v-list-item-title class="text-subtitle-2">
195
- Delete
196
- </v-list-item-title>
197
- </v-list-item>
198
- </v-list>
199
- </v-menu>
200
- </v-col>
201
- </v-row>
143
+ <!-- Fallback: no block definition found for this form type -->
144
+ <v-card v-else width="100%">
145
+ <v-card-text class="text-center pa-10">
146
+ <v-icon size="48" class="mb-3 text-medium-emphasis">mdi-file-question-outline</v-icon>
147
+ <div class="text-body-2 text-medium-emphasis">
148
+ No preview available for "{{ selectedFormType }}".
149
+ </div>
150
+ </v-card-text>
151
+ <v-toolbar density="compact">
152
+ <v-btn tile block variant="text" class="text-none" size="48" @click="previewDialog = false">
153
+ Close
154
+ </v-btn>
202
155
  </v-toolbar>
203
156
  </v-card>
204
157
  </v-dialog>
205
158
 
206
- <!-- Form Render Preview Dialog -->
207
- <OnlineFormRenderPreview
208
- v-model="formRenderPreviewDialog"
209
- :form="selectedOnlineForm"
210
- />
211
-
212
159
  <!-- Delete Dialog -->
213
160
  <v-dialog
214
161
  v-model="confirmDialog"
@@ -272,14 +219,10 @@
272
219
  </template>
273
220
  <script setup lang="ts">
274
221
  import OnlineFormConfigurationForm from "./OnlineFormConfigurationForm.vue";
275
- import OnlineFormRenderPreview from "./OnlineFormRenderPreview.vue";
276
- import useFile from "../composables/useFile";
222
+ import OnlineFormFill from "./OnlineFormFill.vue";
277
223
  import useOnlineFormConfiguration from "../composables/useOnlineFormConfiguration";
278
-
279
- definePageMeta({
280
- middleware: ["01-auth", "02-org"],
281
- memberOnly: true,
282
- });
224
+ import useOnlineFormPages from "../composables/useOnlineFormPages";
225
+ import useUtils from "../composables/useUtils";
283
226
  const props = defineProps({
284
227
  headers: {
285
228
  type: Array as PropType<Array<Record<string, any>>>,
@@ -319,7 +262,7 @@ const props = defineProps({
319
262
  });
320
263
 
321
264
  const { getAll: _getAll, deleteById: _deleteById } = useOnlineFormConfiguration();
322
- const { getFileUrl } = useFile();
265
+ const { formPages } = useOnlineFormPages();
323
266
 
324
267
  const page = ref(1);
325
268
  const pages = ref(0);
@@ -335,6 +278,12 @@ const siteId = route.params.site as string;
335
278
  const orgId = route.params.org as string;
336
279
 
337
280
  const items = ref<Array<Record<string, any>>>([]);
281
+ const sortAlpha = ref(false);
282
+ const sortedItems = computed(() =>
283
+ sortAlpha.value
284
+ ? [...items.value].sort((a, b) => (a.formType ?? "").localeCompare(b.formType ?? ""))
285
+ : items.value
286
+ );
338
287
 
339
288
  const { formatDateDDMMYYYYLocal } = useUtils();
340
289
 
@@ -374,22 +323,16 @@ watchEffect(() => {
374
323
  const createDialog = ref(false);
375
324
  const editDialog = ref(false);
376
325
  const previewDialog = ref(false);
377
- const formRenderPreviewDialog = ref(false);
378
326
  const selectedOnlineForm = ref<TOnlineForm>({
379
327
  _id: "",
380
328
  name: "",
381
329
  createdAt: "",
382
330
  status: "",
383
331
  attachment: "",
384
- inputs: [
385
- {
386
- label: "",
387
- dataType: "",
388
- required: false,
389
- multiple: false,
390
- },
391
- ],
332
+ inputs: [{ label: "", dataType: "", required: false, multiple: false }],
392
333
  });
334
+ const selectedFormType = ref("");
335
+ const selectedFormDefinition = ref<any>(null);
393
336
  const deleteLoading = ref(false);
394
337
  const confirmDialog = ref(false);
395
338
  const selectedOnlineFormId = ref<string | null>(null);
@@ -405,6 +348,17 @@ function parseFieldsToInputs(fields: Record<string, string>): any[] {
405
348
 
406
349
  function tableRowClickHandler(_: any, data: any) {
407
350
  const item = data.item;
351
+ selectedFormType.value = item.formType ?? "";
352
+
353
+ // Parse blocks stored as JSON string, fallback to hardcoded definition
354
+ let def = null;
355
+ if (item.fields?.blocks) {
356
+ try {
357
+ def = { title: item.formType, blocks: JSON.parse(item.fields.blocks) };
358
+ } catch {}
359
+ }
360
+ selectedFormDefinition.value = def ?? formPages[selectedFormType.value] ?? null;
361
+
408
362
  selectedOnlineForm.value = {
409
363
  ...item,
410
364
  inputs: parseFieldsToInputs(item.fields ?? {}),
@@ -475,4 +429,5 @@ function successUpdate() {
475
429
  getOnlineFormConfigurations();
476
430
  showMessage("Online form configuration updated successfully!", "success");
477
431
  }
432
+
478
433
  </script>
@@ -158,7 +158,7 @@
158
158
  height="48"
159
159
  @click="cancel"
160
160
  >
161
- Cancel
161
+ {{ props.cancelText }}
162
162
  </v-btn>
163
163
  </v-col>
164
164
 
@@ -207,6 +207,10 @@ const props = defineProps({
207
207
  type: Boolean,
208
208
  default: true,
209
209
  },
210
+ cancelText: {
211
+ type: String,
212
+ default: "Cancel"
213
+ }
210
214
  });
211
215
 
212
216
  const emit = defineEmits(["cancel", "success", "success:create-more"]);
@@ -1,85 +1,156 @@
1
1
  <template>
2
2
  <div>
3
- <Vue3Signature
4
- ref="signatureRef"
5
- :sigOption="options"
6
- @save="onSave"
7
- style="border: 1px solid #ccc; width: 100%; height: 200px"
8
- />
9
- <div class="mt-4 flex justify-end">
10
- <button @click="clear" class="px-4 py-2 bg-red-500 rounded">Clear</button>
11
- <button @click="save" class="px-4 py-2 bg-green-500 rounded ml-2">
12
- Save
13
- </button>
14
- </div>
15
-
16
- <div style="position: fixed; top: 20px; right: 20px; z-index: 9999">
17
- <Snackbar
18
- v-model="messageSnackbar"
19
- :text="message"
20
- :color="messageColor"
3
+ <div
4
+ style="
5
+ position: relative;
6
+ border: 1.5px dashed #bbb;
7
+ border-radius: 6px;
8
+ background: #fafafa;
9
+ overflow: hidden;
10
+ "
11
+ >
12
+ <canvas
13
+ ref="canvasRef"
14
+ :width="W"
15
+ :height="H"
16
+ style="
17
+ display: block;
18
+ width: 100%;
19
+ height: 130px;
20
+ cursor: crosshair;
21
+ touch-action: none;
22
+ "
23
+ @mousedown="startMouse"
24
+ @mousemove="moveMouse"
25
+ @mouseup="stop"
26
+ @mouseleave="stop"
27
+ @touchstart.prevent="startTouch"
28
+ @touchmove.prevent="moveTouch"
29
+ @touchend="stop"
21
30
  />
31
+ <div
32
+ v-if="isEmpty"
33
+ style="
34
+ position: absolute;
35
+ inset: 0;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ pointer-events: none;
40
+ "
41
+ >
42
+ <span class="text-caption text-disabled">Draw your signature here</span>
43
+ </div>
22
44
  </div>
45
+
46
+ <v-row no-gutters align="center" class="mt-1" style="min-height: 28px">
47
+ <span class="text-caption text-medium-emphasis">{{ label }}</span>
48
+ <v-spacer />
49
+ <v-btn
50
+ v-if="!isEmpty"
51
+ size="x-small"
52
+ variant="text"
53
+ color="error"
54
+ class="text-none"
55
+ @click="clear"
56
+ >
57
+ Clear
58
+ </v-btn>
59
+ </v-row>
23
60
  </div>
24
61
  </template>
25
-
26
62
  <script setup lang="ts">
27
- import { ref, defineProps, defineEmits } from "vue";
28
- import Vue3Signature from "vue3-signature";
29
-
30
63
  const props = defineProps({
31
- modelValue: String,
32
- hideToast: {
33
- type: Boolean,
34
- default: false,
35
- }
64
+ modelValue: { type: String, default: "" },
65
+ label: { type: String, default: "" },
66
+ hideToast: { type: Boolean, default: false },
36
67
  });
37
-
38
68
  const emit = defineEmits(["update:modelValue"]);
39
69
 
40
- const signatureRef = ref();
70
+ // Internal canvas resolution (retina-friendly)
71
+ const W = 960;
72
+ const H = 260;
41
73
 
42
- const options = {
43
- penColor: "black",
44
- backgroundColor: "white",
45
- };
74
+ const canvasRef = ref<HTMLCanvasElement | null>(null);
75
+ const drawing = ref(false);
76
+ const isEmpty = ref(true);
77
+ let px = 0;
78
+ let py = 0;
46
79
 
47
- // Snackbar control
48
- const message = ref("");
49
- const messageColor = ref("");
50
- const messageSnackbar = ref(false);
80
+ function getCtx() {
81
+ const c = canvasRef.value?.getContext("2d");
82
+ if (c) {
83
+ c.strokeStyle = "#111";
84
+ c.lineWidth = 2.5;
85
+ c.lineCap = "round";
86
+ c.lineJoin = "round";
87
+ }
88
+ return c ?? null;
89
+ }
51
90
 
52
- function showMessage(msg: string, color: string) {
53
- message.value = msg;
54
- messageColor.value = color;
55
- messageSnackbar.value = true;
91
+ function scaled(clientX: number, clientY: number) {
92
+ const rect = canvasRef.value!.getBoundingClientRect();
93
+ return {
94
+ x: ((clientX - rect.left) / rect.width) * W,
95
+ y: ((clientY - rect.top) / rect.height) * H,
96
+ };
56
97
  }
57
98
 
58
- function onSave(data: string) {
59
- emit("update:modelValue", data);
60
- if(!props.hideToast) {
61
- showMessage("Signature saved successfully.", "success");
62
- }
99
+ function startMouse(e: MouseEvent) {
100
+ drawing.value = true;
101
+ const p = scaled(e.clientX, e.clientY);
102
+ px = p.x;
103
+ py = p.y;
63
104
  }
64
105
 
65
- function save() {
66
- const signatureData = signatureRef.value?.save("image/jpeg");
67
- if (signatureData) {
68
- emit("update:modelValue", signatureData);
69
- if(!props.hideToast) {
70
- showMessage("Signature saved successfully.", "success");
71
- }
72
- } else {
73
- if(!props.hideToast) {
74
- showMessage("No signature to save.", "error");
75
- }
106
+ function moveMouse(e: MouseEvent) {
107
+ if (!drawing.value) return;
108
+ paint(scaled(e.clientX, e.clientY));
109
+ }
110
+
111
+ function startTouch(e: TouchEvent) {
112
+ drawing.value = true;
113
+ const t = e.touches[0];
114
+ const p = scaled(t.clientX, t.clientY);
115
+ px = p.x;
116
+ py = p.y;
117
+ }
118
+
119
+ function moveTouch(e: TouchEvent) {
120
+ if (!drawing.value) return;
121
+ paint(scaled(e.touches[0].clientX, e.touches[0].clientY));
122
+ }
123
+
124
+ function stop() {
125
+ if (drawing.value && !isEmpty.value) {
126
+ emit("update:modelValue", canvasRef.value!.toDataURL("image/png"));
76
127
  }
128
+ drawing.value = false;
129
+ }
130
+
131
+ function paint({ x, y }: { x: number; y: number }) {
132
+ const c = getCtx();
133
+ if (!c) return;
134
+ c.beginPath();
135
+ c.moveTo(px, py);
136
+ c.lineTo(x, y);
137
+ c.stroke();
138
+ px = x;
139
+ py = y;
140
+ isEmpty.value = false;
77
141
  }
78
142
 
79
143
  function clear() {
80
- signatureRef.value?.clear();
81
- if(!props.hideToast) {
82
- showMessage("Signature cleared.", "success");
83
- }
144
+ const cv = canvasRef.value;
145
+ if (!cv) return;
146
+ cv.getContext("2d")?.clearRect(0, 0, W, H);
147
+ isEmpty.value = true;
148
+ emit("update:modelValue", "");
84
149
  }
150
+
151
+ // Allow parent to programmatically clear by setting modelValue to ""
152
+ watch(
153
+ () => props.modelValue,
154
+ (v) => { if (!v && !isEmpty.value) clear(); }
155
+ );
85
156
  </script>
@@ -109,13 +109,15 @@ export default function useMember() {
109
109
  app: string;
110
110
  siteId?: string;
111
111
  siteName?: string;
112
+ onboardingRequired?: boolean;
112
113
  }) {
113
114
  return useNuxtApp().$api<TMember>("/api/members/direct", {
114
115
  method: "POST",
115
116
  body: payload,
116
- });
117
+ })
117
118
  }
118
119
 
120
+
119
121
  function updateMemberSite(id: string, siteId: string, siteName: string) {
120
122
  return useNuxtApp().$api(`/api/members/site`, {
121
123
  method: "PUT",
@@ -137,6 +139,15 @@ export default function useMember() {
137
139
  );
138
140
  }
139
141
 
142
+ function completeMemberOnboarding(id: string) {
143
+ return useNuxtApp().$api<Record<string, any>>(
144
+ `/api/members/${id}/onboarding/complete`,
145
+ {
146
+ method: "PUT",
147
+ }
148
+ );
149
+ }
150
+
140
151
  return {
141
152
  members,
142
153
  member,
@@ -156,5 +167,6 @@ export default function useMember() {
156
167
  updateMemberSite,
157
168
  getAllByUserId,
158
169
  getByUserTypeOrg,
170
+ completeMemberOnboarding,
159
171
  };
160
172
  }