@7365admin1/layer-common 1.9.0 → 1.10.1
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 +12 -0
- package/components/AcceptDialog.vue +44 -0
- package/components/AccessCardAddForm.vue +101 -13
- package/components/AccessManagement.vue +130 -47
- package/components/AddSupplyForm.vue +165 -0
- package/components/AreaChecklistHistoryLogs.vue +235 -0
- package/components/AreaChecklistHistoryMain.vue +176 -0
- package/components/AreaFormDialog.vue +266 -0
- package/components/AreaMain.vue +841 -0
- package/components/AttendanceCheckInOutDialog.vue +416 -0
- package/components/AttendanceDetailsDialog.vue +184 -0
- package/components/AttendanceMain.vue +155 -0
- package/components/AttendanceMapSearchDialog.vue +393 -0
- package/components/AttendanceSettingsDialog.vue +398 -0
- package/components/BuildingManagement/buildings.vue +5 -5
- package/components/BuildingManagement/units.vue +5 -5
- package/components/ChecklistItemRow.vue +54 -0
- package/components/CheckoutItemMain.vue +705 -0
- package/components/CleaningScheduleMain.vue +271 -0
- package/components/DocumentManagement.vue +8 -9
- package/components/EntryPass/QrTemplatePreview.vue +104 -0
- package/components/EntryPassMain.vue +252 -200
- package/components/HygieneUpdateMoreAction.vue +238 -0
- package/components/IncidentReport/Authorities.vue +226 -0
- package/components/IncidentReport/IncidentInformation.vue +258 -0
- package/components/IncidentReport/affectedEntities.vue +167 -0
- package/components/InvitationMain.vue +19 -17
- package/components/ManageChecklistMain.vue +384 -0
- package/components/MemberMain.vue +48 -20
- package/components/MyAttendanceMain.vue +224 -0
- package/components/OnlineFormsConfiguration.vue +9 -2
- package/components/PasswordConfirmation.vue +95 -0
- package/components/PhotoUpload.vue +410 -0
- package/components/RolePermissionMain.vue +17 -15
- package/components/ScheduleAreaMain.vue +313 -0
- package/components/ScheduleTaskAreaFormDialog.vue +144 -0
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
- package/components/ScheduleTaskForm.vue +471 -0
- package/components/ScheduleTaskMain.vue +345 -0
- package/components/ScheduleTastTicketMain.vue +182 -0
- package/components/ServiceProviderMain.vue +27 -7
- package/components/StockCard.vue +191 -0
- package/components/SupplyManagementMain.vue +557 -0
- package/components/TableHygiene.vue +617 -0
- package/components/UnitMain.vue +451 -0
- package/components/VisitorManagement.vue +28 -15
- package/composables/useAccessManagement.ts +90 -0
- package/composables/useAreaPermission.ts +51 -0
- package/composables/useAreas.ts +99 -0
- package/composables/useAttendance.ts +89 -0
- package/composables/useAttendancePermission.ts +68 -0
- package/composables/useBuilding.ts +2 -2
- package/composables/useBuildingUnit.ts +2 -2
- package/composables/useCard.ts +2 -0
- package/composables/useCheckout.ts +61 -0
- package/composables/useCheckoutPermission.ts +80 -0
- package/composables/useCleaningPermission.ts +229 -0
- package/composables/useCleaningSchedulePermission.ts +58 -0
- package/composables/useCleaningSchedules.ts +233 -0
- package/composables/useCountry.ts +8 -0
- package/composables/useDOBEntries.ts +13 -0
- package/composables/useDashboardData.ts +2 -2
- package/composables/useDocument.ts +3 -2
- package/composables/useFeedback.ts +1 -1
- package/composables/useFile.ts +4 -6
- package/composables/useLocation.ts +78 -0
- package/composables/useOnlineForm.ts +16 -9
- package/composables/usePeople.ts +87 -72
- package/composables/useQR.ts +29 -0
- package/composables/useRole.ts +3 -2
- package/composables/useScheduleTask.ts +89 -0
- package/composables/useScheduleTaskArea.ts +85 -0
- package/composables/useScheduleTaskPermission.ts +68 -0
- package/composables/useSiteEntryPassSettings.ts +4 -15
- package/composables/useStock.ts +45 -0
- package/composables/useSupply.ts +63 -0
- package/composables/useSupplyPermission.ts +92 -0
- package/composables/useUnitPermission.ts +51 -0
- package/composables/useUnits.ts +82 -0
- package/composables/useWebUsb.ts +389 -0
- package/composables/useWorkOrder.ts +1 -1
- package/nuxt.config.ts +3 -0
- package/package.json +4 -1
- package/types/area.d.ts +22 -0
- package/types/attendance.d.ts +38 -0
- package/types/checkout-item.d.ts +27 -0
- package/types/cleaner-schedule.d.ts +54 -0
- package/types/location.d.ts +42 -0
- package/types/schedule-task.d.ts +18 -0
- package/types/stock.d.ts +16 -0
- package/types/supply.d.ts +11 -0
- package/types/verification.d.ts +1 -1
- package/utils/acm-crypto.ts +30 -0
|
@@ -56,8 +56,13 @@
|
|
|
56
56
|
hide-default-footer
|
|
57
57
|
@click:row="tableRowClickHandler"
|
|
58
58
|
style="max-height: calc(100vh - (200px))"
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
>
|
|
60
|
+
<template v-slot:item.createdAt="{ item }">
|
|
61
|
+
{{ formatDateDDMMYYYYLocal(item.createdAt) }}
|
|
62
|
+
</template></v-data-table
|
|
63
|
+
>
|
|
64
|
+
</v-card></v-col
|
|
65
|
+
>
|
|
61
66
|
|
|
62
67
|
<!-- Create Dialog -->
|
|
63
68
|
<v-dialog v-model="createDialog" width="450" persistent>
|
|
@@ -306,6 +311,8 @@ const orgId = route.params.org as string;
|
|
|
306
311
|
|
|
307
312
|
const items = ref<Array<Record<string, any>>>([]);
|
|
308
313
|
|
|
314
|
+
const { formatDateDDMMYYYYLocal } = useUtils();
|
|
315
|
+
|
|
309
316
|
const {
|
|
310
317
|
data: getOnlineFormConfigurationReq,
|
|
311
318
|
refresh: getOnlineFormConfigurations,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%" :disabled="loading || processingPassword" :loading="loading || processingPassword">
|
|
3
|
+
<v-toolbar density="compact">
|
|
4
|
+
<v-row no-gutters class="pa-3 text-h6 font-weight-bold">
|
|
5
|
+
<span>{{ promptTitle }}</span>
|
|
6
|
+
</v-row>
|
|
7
|
+
</v-toolbar>
|
|
8
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5 px-7 text-center">
|
|
9
|
+
<v-row no-gutters class="w-100">
|
|
10
|
+
<v-col cols="12">
|
|
11
|
+
<v-icon icon="mdi-lock-outline" :color="'gray'" class="text-h2" />
|
|
12
|
+
</v-col>
|
|
13
|
+
<v-col cols="12" v-if="message" class="mt-2">
|
|
14
|
+
{{ message }}
|
|
15
|
+
</v-col>
|
|
16
|
+
<v-col cols="12" class="mt-5 text-start">
|
|
17
|
+
<v-text-field v-model="passwordInput" label="Password" type="password" density="compact" :error-messages="errorMessage"></v-text-field>
|
|
18
|
+
</v-col>
|
|
19
|
+
</v-row>
|
|
20
|
+
</v-card-text>
|
|
21
|
+
|
|
22
|
+
<v-toolbar class="pa-0" density="compact">
|
|
23
|
+
<v-row no-gutters>
|
|
24
|
+
<v-col cols="6" class="pa-0">
|
|
25
|
+
<v-btn block variant="text" class="text-none" size="large" tile @click="emit('cancel')" height="48">
|
|
26
|
+
Cancel
|
|
27
|
+
</v-btn>
|
|
28
|
+
</v-col>
|
|
29
|
+
|
|
30
|
+
<v-col cols="6" class="pa-0">
|
|
31
|
+
<v-btn block tile variant="flat" class="text-none" size="large" height="48" color="red" :disabled="!passwordInput"
|
|
32
|
+
@click="handleConfirm">
|
|
33
|
+
Confirm
|
|
34
|
+
</v-btn>
|
|
35
|
+
</v-col>
|
|
36
|
+
</v-row>
|
|
37
|
+
</v-toolbar>
|
|
38
|
+
</v-card>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
|
|
43
|
+
const props = defineProps({
|
|
44
|
+
message: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: "Confirm your digital signature with password to proceed."
|
|
47
|
+
},
|
|
48
|
+
promptTitle: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: "Confirm Action"
|
|
51
|
+
},
|
|
52
|
+
loading: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
default: false
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const { confirmPassword } = useDOBEntries();
|
|
59
|
+
const { currentUser } = useLocalAuth();
|
|
60
|
+
|
|
61
|
+
const emit = defineEmits(["cancel", "confirm"])
|
|
62
|
+
|
|
63
|
+
const processingPassword = ref(false)
|
|
64
|
+
const errorMessage = ref('')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
const passwordInput = ref('')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async function handleConfirm() {
|
|
72
|
+
errorMessage.value = ''
|
|
73
|
+
try {
|
|
74
|
+
processingPassword.value = true;
|
|
75
|
+
const userId = currentUser.value?._id;
|
|
76
|
+
if (!userId) {
|
|
77
|
+
throw new Error('User not found. Please login again.');
|
|
78
|
+
}
|
|
79
|
+
const res = await confirmPassword({ userId, password: passwordInput.value });
|
|
80
|
+
if (res) {
|
|
81
|
+
emit('confirm');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
const errMessage = err?.response?._data?.message || 'Something went wrong. Please try again.';
|
|
86
|
+
console.log('[ERROR]', err)
|
|
87
|
+
errorMessage.value = errMessage;
|
|
88
|
+
} finally {
|
|
89
|
+
processingPassword.value = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<style scoped></style>
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters class="mb-6">
|
|
3
|
+
<v-col cols="12">
|
|
4
|
+
<div class="text-subtitle-2 font-weight-bold mb-3">
|
|
5
|
+
{{ title }}
|
|
6
|
+
<span v-if="required" class="text-error">*</span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<v-sheet
|
|
10
|
+
border
|
|
11
|
+
rounded="lg"
|
|
12
|
+
class="pa-8 text-center upload-area"
|
|
13
|
+
:class="{ 'upload-disabled': uploading }"
|
|
14
|
+
style="cursor: pointer; transition: all 0.2s"
|
|
15
|
+
@click="!uploading && (photoOptionsDialog = true)"
|
|
16
|
+
>
|
|
17
|
+
<v-icon
|
|
18
|
+
:icon="uploading ? 'mdi-loading mdi-spin' : 'mdi-camera-plus-outline'"
|
|
19
|
+
size="56"
|
|
20
|
+
:color="uploading ? 'grey' : 'primary'"
|
|
21
|
+
class="mb-3"
|
|
22
|
+
/>
|
|
23
|
+
<div class="text-body-1 font-weight-medium mb-1">
|
|
24
|
+
{{ uploading ? "Uploading Photo..." : "Add Photo" }}
|
|
25
|
+
</div>
|
|
26
|
+
<div class="text-caption text-medium-emphasis">
|
|
27
|
+
{{ uploading ? "Please wait" : "Take a photo or upload from device" }}
|
|
28
|
+
</div>
|
|
29
|
+
</v-sheet>
|
|
30
|
+
</v-col>
|
|
31
|
+
</v-row>
|
|
32
|
+
|
|
33
|
+
<v-row v-if="photos.length > 0" no-gutters>
|
|
34
|
+
<v-col
|
|
35
|
+
v-for="(photo, index) in photos"
|
|
36
|
+
:key="index"
|
|
37
|
+
cols="auto"
|
|
38
|
+
class="pa-1"
|
|
39
|
+
>
|
|
40
|
+
<div class="position-relative" style="display: inline-block">
|
|
41
|
+
<v-sheet
|
|
42
|
+
rounded="sm"
|
|
43
|
+
style="width: 88px; height: 64px; overflow: hidden; cursor: pointer"
|
|
44
|
+
class="photo-thumb"
|
|
45
|
+
@click="viewPhoto(photo)"
|
|
46
|
+
>
|
|
47
|
+
<v-img :src="photo" height="64" width="88" cover />
|
|
48
|
+
</v-sheet>
|
|
49
|
+
<v-btn
|
|
50
|
+
icon="mdi-close"
|
|
51
|
+
size="x-small"
|
|
52
|
+
variant="tonal"
|
|
53
|
+
color="error"
|
|
54
|
+
class="delete-btn"
|
|
55
|
+
@click.stop="removePhoto(index)"
|
|
56
|
+
style="position: absolute; top: -8px; right: -8px"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</v-col>
|
|
60
|
+
</v-row>
|
|
61
|
+
|
|
62
|
+
<v-dialog v-model="photoOptionsDialog" max-width="420">
|
|
63
|
+
<v-card>
|
|
64
|
+
<v-card-title class="d-flex align-center pa-4">
|
|
65
|
+
<span class="text-h6 font-weight-bold">Add Photo</span>
|
|
66
|
+
<v-spacer />
|
|
67
|
+
<v-btn
|
|
68
|
+
icon="mdi-close"
|
|
69
|
+
variant="text"
|
|
70
|
+
size="small"
|
|
71
|
+
@click="photoOptionsDialog = false"
|
|
72
|
+
/>
|
|
73
|
+
</v-card-title>
|
|
74
|
+
|
|
75
|
+
<v-divider />
|
|
76
|
+
|
|
77
|
+
<v-card-text class="pa-4">
|
|
78
|
+
<v-list class="pa-0" bg-color="transparent">
|
|
79
|
+
<v-list-item class="rounded-lg mb-2" @click="openCameraFromOptions">
|
|
80
|
+
<template v-slot:prepend>
|
|
81
|
+
<v-avatar color="primary" variant="tonal" size="48">
|
|
82
|
+
<v-icon icon="mdi-camera" color="primary" />
|
|
83
|
+
</v-avatar>
|
|
84
|
+
</template>
|
|
85
|
+
<v-list-item-title class="font-weight-medium mb-1">
|
|
86
|
+
Take a Photo
|
|
87
|
+
</v-list-item-title>
|
|
88
|
+
<v-list-item-subtitle class="text-caption">
|
|
89
|
+
Use device camera to capture an image
|
|
90
|
+
</v-list-item-subtitle>
|
|
91
|
+
</v-list-item>
|
|
92
|
+
|
|
93
|
+
<v-list-item class="rounded-lg" @click="triggerFileInput">
|
|
94
|
+
<template v-slot:prepend>
|
|
95
|
+
<v-avatar color="primary" variant="tonal" size="48">
|
|
96
|
+
<v-icon icon="mdi-upload" color="primary" />
|
|
97
|
+
</v-avatar>
|
|
98
|
+
</template>
|
|
99
|
+
<v-list-item-title class="font-weight-medium mb-1">
|
|
100
|
+
Upload a Photo
|
|
101
|
+
</v-list-item-title>
|
|
102
|
+
<v-list-item-subtitle class="text-caption">
|
|
103
|
+
Select an image from your device
|
|
104
|
+
</v-list-item-subtitle>
|
|
105
|
+
</v-list-item>
|
|
106
|
+
</v-list>
|
|
107
|
+
</v-card-text>
|
|
108
|
+
</v-card>
|
|
109
|
+
</v-dialog>
|
|
110
|
+
|
|
111
|
+
<v-dialog v-model="showCameraDialog" max-width="720">
|
|
112
|
+
<v-card>
|
|
113
|
+
<v-toolbar density="compact">
|
|
114
|
+
<v-toolbar-title class="text-subtitle-1 font-weight-medium">
|
|
115
|
+
Camera
|
|
116
|
+
</v-toolbar-title>
|
|
117
|
+
<v-spacer />
|
|
118
|
+
<v-btn icon @click="closeCameraDialog">
|
|
119
|
+
<v-icon>mdi-close</v-icon>
|
|
120
|
+
</v-btn>
|
|
121
|
+
</v-toolbar>
|
|
122
|
+
<v-card-text>
|
|
123
|
+
<div class="d-flex flex-column align-center">
|
|
124
|
+
<div style="width: 100%; max-width: 640px">
|
|
125
|
+
<video
|
|
126
|
+
ref="videoRef"
|
|
127
|
+
autoplay
|
|
128
|
+
muted
|
|
129
|
+
playsinline
|
|
130
|
+
style="width: 100%; border-radius: 8px; background: #000"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
<canvas ref="canvasRef" style="display: none"></canvas>
|
|
134
|
+
<div class="text-caption grey--text mt-2">
|
|
135
|
+
Tip: Allow camera permissions to use this feature.
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</v-card-text>
|
|
139
|
+
<v-card-actions>
|
|
140
|
+
<v-btn
|
|
141
|
+
text
|
|
142
|
+
@click="closeCameraDialog"
|
|
143
|
+
class="text-none font-weight-medium"
|
|
144
|
+
>
|
|
145
|
+
CANCEL
|
|
146
|
+
</v-btn>
|
|
147
|
+
<v-spacer />
|
|
148
|
+
<v-btn
|
|
149
|
+
block
|
|
150
|
+
variant="flat"
|
|
151
|
+
color="black"
|
|
152
|
+
class="text-none font-weight-bold rounded-0"
|
|
153
|
+
height="56"
|
|
154
|
+
size="large"
|
|
155
|
+
@click="capturePhoto"
|
|
156
|
+
>
|
|
157
|
+
<v-icon class="mr-2">mdi-camera</v-icon>
|
|
158
|
+
CAPTURE
|
|
159
|
+
</v-btn>
|
|
160
|
+
</v-card-actions>
|
|
161
|
+
</v-card>
|
|
162
|
+
</v-dialog>
|
|
163
|
+
|
|
164
|
+
<input
|
|
165
|
+
ref="fileInputRef"
|
|
166
|
+
type="file"
|
|
167
|
+
accept="image/*"
|
|
168
|
+
style="display: none"
|
|
169
|
+
@change="onFileSelected"
|
|
170
|
+
/>
|
|
171
|
+
</template>
|
|
172
|
+
|
|
173
|
+
<script setup lang="ts">
|
|
174
|
+
const props = defineProps({
|
|
175
|
+
title: {
|
|
176
|
+
type: String,
|
|
177
|
+
default: "Photos (Optional)",
|
|
178
|
+
},
|
|
179
|
+
required: {
|
|
180
|
+
type: Boolean,
|
|
181
|
+
default: false,
|
|
182
|
+
},
|
|
183
|
+
modelValue: {
|
|
184
|
+
type: Array as PropType<string[]>,
|
|
185
|
+
default: () => [],
|
|
186
|
+
},
|
|
187
|
+
photoIds: {
|
|
188
|
+
type: Array as PropType<string[]>,
|
|
189
|
+
default: () => [],
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const emit = defineEmits(["update:modelValue", "update:photoIds", "error"]);
|
|
194
|
+
|
|
195
|
+
const photos = ref<string[]>([...props.modelValue]);
|
|
196
|
+
const photosIds = ref<string[]>([...props.photoIds]);
|
|
197
|
+
const photoOptionsDialog = ref(false);
|
|
198
|
+
const showCameraDialog = ref(false);
|
|
199
|
+
const videoRef = ref<HTMLVideoElement | null>(null);
|
|
200
|
+
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
201
|
+
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
202
|
+
let mediaStream: MediaStream | null = null;
|
|
203
|
+
const uploading = ref(false);
|
|
204
|
+
|
|
205
|
+
const { addFile } = useFile();
|
|
206
|
+
|
|
207
|
+
function viewPhoto(photoData: string) {
|
|
208
|
+
const link = document.createElement("a");
|
|
209
|
+
link.href = photoData;
|
|
210
|
+
link.target = "_blank";
|
|
211
|
+
link.click();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
watch(
|
|
215
|
+
() => props.modelValue,
|
|
216
|
+
(newVal) => {
|
|
217
|
+
if (JSON.stringify(newVal) !== JSON.stringify(photos.value)) {
|
|
218
|
+
photos.value = [...newVal];
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
watch(
|
|
224
|
+
() => props.photoIds,
|
|
225
|
+
(newVal) => {
|
|
226
|
+
if (JSON.stringify(newVal) !== JSON.stringify(photosIds.value)) {
|
|
227
|
+
photosIds.value = [...newVal];
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
watch(
|
|
233
|
+
photos,
|
|
234
|
+
(newVal) => {
|
|
235
|
+
if (JSON.stringify(newVal) !== JSON.stringify(props.modelValue)) {
|
|
236
|
+
emit("update:modelValue", newVal);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
{ deep: true },
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
watch(
|
|
243
|
+
photosIds,
|
|
244
|
+
(newVal) => {
|
|
245
|
+
if (JSON.stringify(newVal) !== JSON.stringify(props.photoIds)) {
|
|
246
|
+
emit("update:photoIds", newVal);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{ deep: true },
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
function triggerFileInput() {
|
|
253
|
+
photoOptionsDialog.value = false;
|
|
254
|
+
fileInputRef.value?.click();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function onFileSelected(e: Event) {
|
|
258
|
+
const input = e.target as HTMLInputElement;
|
|
259
|
+
const file = input.files?.[0];
|
|
260
|
+
if (!file) return;
|
|
261
|
+
|
|
262
|
+
const reader = new FileReader();
|
|
263
|
+
reader.onload = async () => {
|
|
264
|
+
try {
|
|
265
|
+
if (typeof reader.result === "string") {
|
|
266
|
+
photos.value = [...photos.value, reader.result];
|
|
267
|
+
}
|
|
268
|
+
uploading.value = true;
|
|
269
|
+
const uploadRes = await addFile(file);
|
|
270
|
+
if (uploadRes?.id) {
|
|
271
|
+
photosIds.value = [...photosIds.value, uploadRes.id];
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error("File upload error", err);
|
|
275
|
+
emit("error", "Failed to upload image");
|
|
276
|
+
|
|
277
|
+
if (photos.value.length > photosIds.value.length) {
|
|
278
|
+
photos.value.pop();
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
uploading.value = false;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
reader.readAsDataURL(file);
|
|
285
|
+
input.value = "";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function openCameraFromOptions() {
|
|
289
|
+
photoOptionsDialog.value = false;
|
|
290
|
+
showCameraDialog.value = true;
|
|
291
|
+
try {
|
|
292
|
+
mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
293
|
+
video: true,
|
|
294
|
+
audio: false,
|
|
295
|
+
});
|
|
296
|
+
if (videoRef.value) {
|
|
297
|
+
videoRef.value.srcObject = mediaStream;
|
|
298
|
+
await videoRef.value.play();
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error("Camera error", err);
|
|
302
|
+
emit("error", "Unable to access camera");
|
|
303
|
+
showCameraDialog.value = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function capturePhoto() {
|
|
308
|
+
if (!videoRef.value) return;
|
|
309
|
+
|
|
310
|
+
const video = videoRef.value;
|
|
311
|
+
const canvas = canvasRef.value || document.createElement("canvas");
|
|
312
|
+
canvas.width = video.videoWidth || 640;
|
|
313
|
+
canvas.height = video.videoHeight || 480;
|
|
314
|
+
const ctx = canvas.getContext("2d");
|
|
315
|
+
if (!ctx) return;
|
|
316
|
+
|
|
317
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
318
|
+
const data = canvas.toDataURL("image/jpeg", 0.9);
|
|
319
|
+
photos.value = [...photos.value, data];
|
|
320
|
+
|
|
321
|
+
// Convert to blob and upload
|
|
322
|
+
canvas.toBlob(async (blob) => {
|
|
323
|
+
if (!blob) {
|
|
324
|
+
closeCameraDialog();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const file = new File([blob], `photo-${Date.now()}.jpg`, {
|
|
329
|
+
type: "image/jpeg",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
uploading.value = true;
|
|
334
|
+
const uploadRes = await addFile(file);
|
|
335
|
+
if (uploadRes?.id) {
|
|
336
|
+
photosIds.value = [...photosIds.value, uploadRes.id];
|
|
337
|
+
}
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error("Capture upload error", err);
|
|
340
|
+
emit("error", "Failed to upload captured image");
|
|
341
|
+
// Remove the preview if upload failed
|
|
342
|
+
if (photos.value.length > photosIds.value.length) {
|
|
343
|
+
photos.value.pop();
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
uploading.value = false;
|
|
347
|
+
closeCameraDialog();
|
|
348
|
+
}
|
|
349
|
+
}, "image/jpeg");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function closeCameraDialog() {
|
|
353
|
+
showCameraDialog.value = false;
|
|
354
|
+
if (mediaStream) {
|
|
355
|
+
mediaStream.getTracks().forEach((t) => t.stop());
|
|
356
|
+
mediaStream = null;
|
|
357
|
+
}
|
|
358
|
+
if (videoRef.value) {
|
|
359
|
+
videoRef.value.pause();
|
|
360
|
+
videoRef.value.srcObject = null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function removePhoto(index: number) {
|
|
365
|
+
if (index < 0 || index >= photos.value.length) return;
|
|
366
|
+
photos.value.splice(index, 1);
|
|
367
|
+
if (photosIds.value && photosIds.value.length > index) {
|
|
368
|
+
photosIds.value.splice(index, 1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
onBeforeUnmount(() => {
|
|
373
|
+
if (mediaStream) {
|
|
374
|
+
mediaStream.getTracks().forEach((t) => t.stop());
|
|
375
|
+
mediaStream = null;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
</script>
|
|
379
|
+
|
|
380
|
+
<style scoped>
|
|
381
|
+
.upload-area {
|
|
382
|
+
&:hover:not(.upload-disabled) {
|
|
383
|
+
border-color: rgb(var(--v-theme-primary)) !important;
|
|
384
|
+
background-color: rgba(var(--v-theme-primary), 0.04);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.upload-disabled {
|
|
389
|
+
cursor: not-allowed !important;
|
|
390
|
+
opacity: 0.6;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.photo-thumb {
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
transition: transform 0.2s;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.photo-thumb:hover {
|
|
399
|
+
transform: scale(1.05);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.delete-btn {
|
|
403
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
404
|
+
transition: all 0.2s;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.delete-btn:hover {
|
|
408
|
+
transform: scale(1.1);
|
|
409
|
+
}
|
|
410
|
+
</style>
|
|
@@ -119,21 +119,23 @@
|
|
|
119
119
|
</template>
|
|
120
120
|
|
|
121
121
|
<template #footer>
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
122
|
+
<div class="d-flex justify-center ga-3 w-100">
|
|
123
|
+
<v-btn
|
|
124
|
+
variant="text"
|
|
125
|
+
@click="confirmDialog = false"
|
|
126
|
+
:disabled="deleteLoading"
|
|
127
|
+
>
|
|
128
|
+
Close
|
|
129
|
+
</v-btn>
|
|
130
|
+
<v-btn
|
|
131
|
+
color="primary"
|
|
132
|
+
variant="flat"
|
|
133
|
+
@click="handleDeleteRole"
|
|
134
|
+
:loading="deleteLoading"
|
|
135
|
+
>
|
|
136
|
+
Delete Role
|
|
137
|
+
</v-btn>
|
|
138
|
+
</div>
|
|
137
139
|
</template>
|
|
138
140
|
</ConfirmDialog>
|
|
139
141
|
|