@7365admin1/layer-common 1.10.8 → 1.10.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 +12 -0
- package/components/AccessCardAddForm.vue +1 -1
- package/components/AccessCardAssignToUnitForm.vue +1 -1
- package/components/AccessManagement.vue +1 -1
- package/components/BulletinBoardManagement.vue +18 -8
- package/components/Carousel.vue +474 -0
- package/components/DeliveryCompany.vue +240 -0
- package/components/DrawImage.vue +172 -0
- package/components/EntryPassInformation.vue +70 -10
- package/components/EquipmentItemMain.vue +9 -4
- package/components/Feedback/Form.vue +4 -4
- package/components/FeedbackMain.vue +734 -146
- package/components/FileInput.vue +289 -0
- package/components/IncidentReport/Authorities.vue +189 -151
- package/components/IncidentReport/IncidentInformation.vue +14 -10
- package/components/IncidentReport/IncidentInformationDownload.vue +212 -0
- package/components/IncidentReport/affectedEntities.vue +8 -57
- package/components/SiteSettings.vue +285 -0
- package/components/StockCard.vue +11 -7
- package/components/Tooltip/Info.vue +33 -0
- package/components/VisitorForm.vue +176 -45
- package/components/VisitorManagement.vue +23 -6
- package/composables/useAccessManagement.ts +60 -18
- package/composables/useBulletin.ts +8 -3
- package/composables/useBulletinBoardPermission.ts +48 -0
- package/composables/useCleaningPermission.ts +2 -0
- package/composables/useCommonPermission.ts +29 -1
- package/composables/useEquipmentManagement.ts +63 -0
- package/composables/useFeedback.ts +53 -21
- package/composables/useFile.ts +6 -0
- package/composables/useLocalAuth.ts +29 -1
- package/composables/useSiteSettings.ts +1 -1
- package/composables/useUploadFiles.ts +94 -0
- package/composables/useUtils.ts +152 -53
- package/composables/useVisitor.ts +9 -6
- package/constants/app.ts +12 -0
- package/nuxt.config.ts +2 -0
- package/package.json +3 -1
- package/plugins/vue-draggable-next.client.ts +5 -0
- package/types/feedback.d.ts +5 -2
- package/types/site.d.ts +2 -1
- package/types/user.d.ts +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters class="pa-3">
|
|
3
|
+
<v-col cols="12">
|
|
4
|
+
<v-card width="100%" variant="outlined" border="thin" rounded="lg" max-width="700" max-height="500"
|
|
5
|
+
style="overflow-y: auto;">
|
|
6
|
+
<v-toolbar density="compact" color="grey-lighten-4 pl-2 pr-4">
|
|
7
|
+
<v-row v-if="!hideTitle" no-gutters class="px-3 font-weight-bold">
|
|
8
|
+
Delivery Companies
|
|
9
|
+
</v-row>
|
|
10
|
+
|
|
11
|
+
<template #append>
|
|
12
|
+
<v-btn v-if="!props.readOnly" variant="flat" color="primary" class="text-none"
|
|
13
|
+
@click="handleAdd">
|
|
14
|
+
Add
|
|
15
|
+
</v-btn>
|
|
16
|
+
</template>
|
|
17
|
+
</v-toolbar>
|
|
18
|
+
|
|
19
|
+
<v-card-text class="pa-0" style="min-height: 100px;">
|
|
20
|
+
<template v-if="companies.length > 0">
|
|
21
|
+
<draggable :list="companies" item-key="index" class="drag-area"
|
|
22
|
+
:disabled="editingIndex !== null">
|
|
23
|
+
<v-list-item v-for="(item, index) in companies" :key="index" class="mt-1">
|
|
24
|
+
<v-row class="d-flex justify-space-between align-center" no-gutters>
|
|
25
|
+
<span class="d-flex align-center">
|
|
26
|
+
<v-btn icon="mdi-drag" class="drag-handle text-grey mr-2" flat
|
|
27
|
+
density="compact" />
|
|
28
|
+
|
|
29
|
+
<div v-if="editingIndex === index" class="w-full">
|
|
30
|
+
<v-text-field v-model="editedValue" class="text-subtitle-2"
|
|
31
|
+
prepend-icon="mdi-truck" style="min-width: 300px;" density="compact"
|
|
32
|
+
hide-details autofocus />
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div v-else>
|
|
36
|
+
<span>
|
|
37
|
+
<v-icon icon="mdi-truck" class="text-h6 mr-2" />
|
|
38
|
+
</span>
|
|
39
|
+
{{ item }}
|
|
40
|
+
</div>
|
|
41
|
+
</span>
|
|
42
|
+
|
|
43
|
+
<span class="d-flex align-center ga-2">
|
|
44
|
+
<template v-if="editingIndex === index">
|
|
45
|
+
<v-btn icon="mdi-check" color="green" density="compact"
|
|
46
|
+
@click="saveEdit(index)" />
|
|
47
|
+
<v-btn icon="mdi-close" color="grey" density="compact"
|
|
48
|
+
@click="cancelEdit" />
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<template v-else>
|
|
52
|
+
<v-btn v-if="!props.readOnly" flat icon="mdi-pencil" class="text-grey"
|
|
53
|
+
density="compact" @click="startEdit(index)" />
|
|
54
|
+
<v-btn v-if="!props.readOnly" flat icon="mdi-trash-outline" class="text-red"
|
|
55
|
+
density="compact" @click="handleRemove(index)" />
|
|
56
|
+
</template>
|
|
57
|
+
</span>
|
|
58
|
+
</v-row>
|
|
59
|
+
</v-list-item>
|
|
60
|
+
</draggable>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<div v-else class="text-center py-5">
|
|
64
|
+
No delivery companies added yet.
|
|
65
|
+
</div>
|
|
66
|
+
</v-card-text>
|
|
67
|
+
</v-card>
|
|
68
|
+
</v-col>
|
|
69
|
+
|
|
70
|
+
<v-dialog v-model="dialog.add" persistent width="450">
|
|
71
|
+
<v-card width="100%">
|
|
72
|
+
<v-toolbar>
|
|
73
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
74
|
+
<span class="font-weight-bold text-h5 text-capitalize">
|
|
75
|
+
Add Delivery Company
|
|
76
|
+
</span>
|
|
77
|
+
</v-row>
|
|
78
|
+
</v-toolbar>
|
|
79
|
+
|
|
80
|
+
<v-card-text>
|
|
81
|
+
<v-text-field v-model="editedValue" placeholder="Company Name" autofocus />
|
|
82
|
+
</v-card-text>
|
|
83
|
+
|
|
84
|
+
<v-toolbar>
|
|
85
|
+
<v-row no-gutters justify="end" align="center" class="px-5">
|
|
86
|
+
<v-btn variant="text" text="Cancel" @click="dialog.add = false" />
|
|
87
|
+
<v-btn variant="flat" color="primary" class="text-none ml-2"
|
|
88
|
+
@click="saveEdit(companies.length)">
|
|
89
|
+
Add
|
|
90
|
+
</v-btn>
|
|
91
|
+
</v-row>
|
|
92
|
+
</v-toolbar>
|
|
93
|
+
</v-card>
|
|
94
|
+
</v-dialog>
|
|
95
|
+
|
|
96
|
+
<Snackbar v-model="toast.show" :text="toast.message" :color="toast.color" />
|
|
97
|
+
|
|
98
|
+
<v-dialog v-model="dialog.delete" persistent width="450">
|
|
99
|
+
<CardDeleteConfirmation
|
|
100
|
+
:prompt-title="`Are you sure you want to delete this ${companies?.[deleteIndex!]}?`"
|
|
101
|
+
@close="dialog.delete = false"
|
|
102
|
+
@delete="removeCompany(deleteIndex!)"
|
|
103
|
+
/>
|
|
104
|
+
</v-dialog>
|
|
105
|
+
</v-row>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<script setup lang="ts">
|
|
109
|
+
import useSiteSettings from '../composables/useSiteSettings'
|
|
110
|
+
|
|
111
|
+
const props = defineProps<{
|
|
112
|
+
site: string
|
|
113
|
+
readOnly?: boolean
|
|
114
|
+
hideTitle?: boolean
|
|
115
|
+
}>()
|
|
116
|
+
|
|
117
|
+
const emit = defineEmits<{
|
|
118
|
+
(e: 'update:companiesValue', value: string[]): void
|
|
119
|
+
(e: 'refresh-site'): void
|
|
120
|
+
}>()
|
|
121
|
+
|
|
122
|
+
const { updateSitebyId } = useSiteSettings()
|
|
123
|
+
|
|
124
|
+
const initialCompanies = defineModel<string[]>('initial', {
|
|
125
|
+
type: Array,
|
|
126
|
+
default: []
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const dialog = reactive({
|
|
130
|
+
add: false,
|
|
131
|
+
delete: false,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const toast = reactive({
|
|
135
|
+
show: false,
|
|
136
|
+
message: '',
|
|
137
|
+
color: 'success',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const companies = ref<string[]>([])
|
|
141
|
+
const editingIndex = ref<number | null>(null)
|
|
142
|
+
const editedValue = ref('')
|
|
143
|
+
const isHydrating = ref(true)
|
|
144
|
+
const deleteIndex = ref<number | null>(null)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
watch(
|
|
149
|
+
companies,
|
|
150
|
+
async (newVal) => {
|
|
151
|
+
await persistCompanies()
|
|
152
|
+
},
|
|
153
|
+
{ deep: true, immediate: false }
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const startEdit = (index: number) => {
|
|
157
|
+
editingIndex.value = index
|
|
158
|
+
editedValue.value = companies.value[index] || ''
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const saveEdit = async (index: number) => {
|
|
162
|
+
const value = editedValue.value.trim()
|
|
163
|
+
if (!value) return
|
|
164
|
+
|
|
165
|
+
const updated = [...companies.value]
|
|
166
|
+
|
|
167
|
+
if (index >= updated.length) {
|
|
168
|
+
updated.push(value)
|
|
169
|
+
} else {
|
|
170
|
+
updated[index] = value
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
companies.value = updated
|
|
174
|
+
editingIndex.value = null
|
|
175
|
+
editedValue.value = ''
|
|
176
|
+
dialog.add = false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cancelEdit = () => {
|
|
180
|
+
editingIndex.value = null
|
|
181
|
+
editedValue.value = ''
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const handleAdd = () => {
|
|
185
|
+
dialog.add = true
|
|
186
|
+
editedValue.value = ''
|
|
187
|
+
editingIndex.value = null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const removeCompany = async (index: number) => {
|
|
191
|
+
const updated = [...companies.value]
|
|
192
|
+
updated.splice(index, 1)
|
|
193
|
+
companies.value = updated
|
|
194
|
+
dialog.delete = false
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const handleRemove = async (index: number) => {
|
|
198
|
+
deleteIndex.value = index;
|
|
199
|
+
dialog.delete = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const persistCompanies = async () => {
|
|
203
|
+
try {
|
|
204
|
+
const isSame =
|
|
205
|
+
companies.value.length === initialCompanies.value.length &&
|
|
206
|
+
companies.value.every((c, i) => c === initialCompanies.value[i]);
|
|
207
|
+
if (isSame) return
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
await updateSitebyId(props.site, {
|
|
211
|
+
deliveryCompanyList: companies.value,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
toast.message = 'Delivery companies updated successfully!'
|
|
215
|
+
toast.color = 'success'
|
|
216
|
+
toast.show = true
|
|
217
|
+
emit('refresh-site')
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('Failed to update delivery companies:', error)
|
|
220
|
+
toast.message = 'Failed to update delivery companies. Please try again.'
|
|
221
|
+
toast.color = 'error'
|
|
222
|
+
toast.show = true
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
watch(initialCompanies, (val) => {
|
|
227
|
+
companies.value = Array.isArray(val) ? [...val] : []
|
|
228
|
+
isHydrating.value = false
|
|
229
|
+
}, { immediate: true })
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<style scoped>
|
|
233
|
+
.drag-area {
|
|
234
|
+
cursor: grab;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.drag-handle {
|
|
238
|
+
cursor: grab;
|
|
239
|
+
}
|
|
240
|
+
</style>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog
|
|
3
|
+
v-model="isDialogVisible"
|
|
4
|
+
transition="dialog-top-transition"
|
|
5
|
+
max-width="1000"
|
|
6
|
+
>
|
|
7
|
+
<v-card class="pa-1">
|
|
8
|
+
<v-toolbar>
|
|
9
|
+
<v-spacer></v-spacer>
|
|
10
|
+
<v-btn icon="mdi-close" @click="emit('onCloseDialog')" />
|
|
11
|
+
</v-toolbar>
|
|
12
|
+
<canvas id="imageCanvas" />
|
|
13
|
+
<v-card-title class="text-end">
|
|
14
|
+
<v-btn
|
|
15
|
+
color="orange-darken-3"
|
|
16
|
+
size="small"
|
|
17
|
+
variant="flat"
|
|
18
|
+
style="height: 40px; margin-right: 12px"
|
|
19
|
+
@click="onClearDrawing()"
|
|
20
|
+
>
|
|
21
|
+
Clear Drawing
|
|
22
|
+
</v-btn>
|
|
23
|
+
<v-btn
|
|
24
|
+
color="blue-darken-3"
|
|
25
|
+
size="small"
|
|
26
|
+
variant="flat"
|
|
27
|
+
style="height: 40px"
|
|
28
|
+
@click="submit"
|
|
29
|
+
>
|
|
30
|
+
Submit
|
|
31
|
+
</v-btn>
|
|
32
|
+
</v-card-title>
|
|
33
|
+
</v-card>
|
|
34
|
+
</v-dialog>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
const { getImage } = useUtils();
|
|
39
|
+
|
|
40
|
+
const props = defineProps({
|
|
41
|
+
message: {
|
|
42
|
+
type: String,
|
|
43
|
+
},
|
|
44
|
+
isShowDialog: {
|
|
45
|
+
type: Boolean,
|
|
46
|
+
default: false,
|
|
47
|
+
},
|
|
48
|
+
imageUrl: {
|
|
49
|
+
type: String,
|
|
50
|
+
},
|
|
51
|
+
imageIdx: {
|
|
52
|
+
type: Number,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const emit = defineEmits<{
|
|
56
|
+
(event: "onSubmit", value: string, idx: number): void;
|
|
57
|
+
(event: "onCloseDialog"): void;
|
|
58
|
+
}>();
|
|
59
|
+
|
|
60
|
+
const isDialogVisible = computed(() => props.isShowDialog);
|
|
61
|
+
|
|
62
|
+
var canvas: any = document.getElementById("imageCanvas");
|
|
63
|
+
var ctx: any = "";
|
|
64
|
+
var img: any = "";
|
|
65
|
+
var pos: any = { x: 0, y: 0 };
|
|
66
|
+
|
|
67
|
+
onMounted(async () => {
|
|
68
|
+
canvas = document.getElementById("imageCanvas");
|
|
69
|
+
ctx = canvas?.getContext("2d");
|
|
70
|
+
|
|
71
|
+
var rect = canvas.getBoundingClientRect();
|
|
72
|
+
canvas.width = rect.width < 712 ? 712 : rect.width;
|
|
73
|
+
canvas.height = rect.height < 400 ? 400 : rect.height;
|
|
74
|
+
|
|
75
|
+
const response = await getImage(props.imageUrl!);
|
|
76
|
+
if (!response) return;
|
|
77
|
+
handleImage(response);
|
|
78
|
+
|
|
79
|
+
canvas.addEventListener("mousedown", setPosition);
|
|
80
|
+
canvas.addEventListener("mousemove", draw);
|
|
81
|
+
canvas.addEventListener(
|
|
82
|
+
"touchstart",
|
|
83
|
+
function (e: any) {
|
|
84
|
+
var touch = e.touches[0];
|
|
85
|
+
var mouseEvent = new MouseEvent("mousedown", {
|
|
86
|
+
buttons: 1,
|
|
87
|
+
clientX: touch.clientX,
|
|
88
|
+
clientY: touch.clientY,
|
|
89
|
+
});
|
|
90
|
+
canvas.dispatchEvent(mouseEvent);
|
|
91
|
+
},
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
94
|
+
canvas.addEventListener(
|
|
95
|
+
"touchmove",
|
|
96
|
+
function (e: any) {
|
|
97
|
+
var touch = e.touches[0];
|
|
98
|
+
var mouseEvent = new MouseEvent("mousemove", {
|
|
99
|
+
buttons: 1,
|
|
100
|
+
clientX: touch.clientX,
|
|
101
|
+
clientY: touch.clientY,
|
|
102
|
+
});
|
|
103
|
+
canvas.dispatchEvent(mouseEvent);
|
|
104
|
+
},
|
|
105
|
+
false,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
var imageCanvas: any = document.getElementById("imageCanvas");
|
|
109
|
+
imageCanvas.style.cursor = "crosshair";
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function handleImage(image: Blob) {
|
|
113
|
+
var reader = new FileReader();
|
|
114
|
+
reader.onload = function (event: any) {
|
|
115
|
+
img = new Image();
|
|
116
|
+
img.onload = function () {
|
|
117
|
+
var ratio = this.height / this.width;
|
|
118
|
+
canvas.height = canvas.width * ratio;
|
|
119
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
120
|
+
};
|
|
121
|
+
img.src = event.target.result;
|
|
122
|
+
};
|
|
123
|
+
reader.readAsDataURL(image);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function setPosition(e: any) {
|
|
127
|
+
var rect = canvas.getBoundingClientRect();
|
|
128
|
+
|
|
129
|
+
if (rect.width < 712 && rect.height < 400) {
|
|
130
|
+
var scaleX = 712 / rect.width;
|
|
131
|
+
var scaleY = 400 / rect.height;
|
|
132
|
+
|
|
133
|
+
pos.x = (e.clientX - rect.left) * scaleX;
|
|
134
|
+
pos.y = (e.clientY - rect.top) * scaleY;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pos.x = e.clientX - rect.left;
|
|
139
|
+
pos.y = e.clientY - rect.top;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function draw(e: any) {
|
|
143
|
+
canvas.style.cursor = "crosshair";
|
|
144
|
+
|
|
145
|
+
if (e.buttons !== 1) return;
|
|
146
|
+
if (pos.x === 0 && pos.y === 0) setPosition(e);
|
|
147
|
+
|
|
148
|
+
ctx.beginPath();
|
|
149
|
+
ctx.lineWidth = 5;
|
|
150
|
+
ctx.lineCap = "round";
|
|
151
|
+
ctx.strokeStyle = "#FF0000";
|
|
152
|
+
ctx.moveTo(pos.x, pos.y);
|
|
153
|
+
setPosition(e);
|
|
154
|
+
ctx.lineTo(pos.x, pos.y);
|
|
155
|
+
ctx.stroke();
|
|
156
|
+
ctx.closePath();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const submit = () => {
|
|
160
|
+
canvas.toBlob((blob: any) => {
|
|
161
|
+
const url = URL.createObjectURL(blob);
|
|
162
|
+
console.log("URL", url);
|
|
163
|
+
|
|
164
|
+
emit("onSubmit", url, props.imageIdx!);
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
async function onClearDrawing() {
|
|
169
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
170
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
@@ -46,12 +46,19 @@
|
|
|
46
46
|
No printer is configured in the settings. QR Pass may not print.
|
|
47
47
|
</v-alert>
|
|
48
48
|
<template v-else>
|
|
49
|
-
<
|
|
49
|
+
<div class="d-flex align-center justify-space-between mt-3">
|
|
50
|
+
<InputLabel title="Quantity" />
|
|
51
|
+
<span v-if="availableCount !== null" class="text-caption text-grey">
|
|
52
|
+
{{ availableCount }} available
|
|
53
|
+
</span>
|
|
54
|
+
<v-progress-circular v-else-if="cardsLoading" size="14" width="2" indeterminate color="grey" />
|
|
55
|
+
</div>
|
|
50
56
|
<v-text-field
|
|
51
57
|
v-model.number="quantity"
|
|
52
58
|
type="number"
|
|
53
59
|
density="comfortable"
|
|
54
60
|
:min="1"
|
|
61
|
+
:max="availableCount ?? undefined"
|
|
55
62
|
hide-details
|
|
56
63
|
/>
|
|
57
64
|
</template>
|
|
@@ -65,7 +72,7 @@
|
|
|
65
72
|
v-model="selectedCards"
|
|
66
73
|
:items="cardItems"
|
|
67
74
|
:loading="cardsLoading"
|
|
68
|
-
item-title="
|
|
75
|
+
item-title="cardNo"
|
|
69
76
|
item-value="_id"
|
|
70
77
|
density="comfortable"
|
|
71
78
|
multiple
|
|
@@ -78,7 +85,7 @@
|
|
|
78
85
|
<template #item="{ props: itemProps, item }">
|
|
79
86
|
<v-list-item v-bind="itemProps">
|
|
80
87
|
<template #subtitle>
|
|
81
|
-
<span class="text-caption text-grey">{{ item.raw.
|
|
88
|
+
<span class="text-caption text-grey">{{ item.raw.cardNo }}</span>
|
|
82
89
|
</template>
|
|
83
90
|
</v-list-item>
|
|
84
91
|
</template>
|
|
@@ -182,6 +189,8 @@
|
|
|
182
189
|
</v-card>
|
|
183
190
|
</v-container>
|
|
184
191
|
</v-dialog>
|
|
192
|
+
|
|
193
|
+
<Snackbar v-model="snackbar" :text="snackbarText" :color="snackbarColor" />
|
|
185
194
|
</div>
|
|
186
195
|
</template>
|
|
187
196
|
|
|
@@ -207,11 +216,19 @@ const props = defineProps({
|
|
|
207
216
|
type: Array as PropType<any[]>,
|
|
208
217
|
default: () => [],
|
|
209
218
|
},
|
|
219
|
+
siteId: {
|
|
220
|
+
type: String as PropType<string | null>,
|
|
221
|
+
default: null,
|
|
222
|
+
},
|
|
223
|
+
unitId: {
|
|
224
|
+
type: String as PropType<string | null>,
|
|
225
|
+
default: null,
|
|
226
|
+
},
|
|
210
227
|
});
|
|
211
228
|
|
|
212
229
|
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards"]);
|
|
213
230
|
|
|
214
|
-
const {
|
|
231
|
+
const { getAvailableContractorCards, generateQrVms } = useAccessManagement();
|
|
215
232
|
|
|
216
233
|
const nfcEnabled = computed(() => props.settings?.data?.settings?.nfcPass ?? false);
|
|
217
234
|
const printer = computed(() => props.settings?.data?.settings?.printer ?? { vendorId: null, productId: null });
|
|
@@ -235,15 +252,54 @@ const selectedCards = computed({
|
|
|
235
252
|
},
|
|
236
253
|
});
|
|
237
254
|
|
|
255
|
+
// ─── Snackbar ─────────────────────────────────────────────────────
|
|
256
|
+
const snackbar = ref(false);
|
|
257
|
+
const snackbarText = ref("");
|
|
258
|
+
const snackbarColor = ref("error");
|
|
259
|
+
|
|
260
|
+
function showSnackbar(text: string, color = "error") {
|
|
261
|
+
snackbarText.value = text;
|
|
262
|
+
snackbarColor.value = color;
|
|
263
|
+
snackbar.value = true;
|
|
264
|
+
}
|
|
265
|
+
|
|
238
266
|
// ─── Card fetching ────────────────────────────────────────────────
|
|
239
267
|
const cardItems = ref<any[]>([]);
|
|
240
268
|
const cardsLoading = ref(false);
|
|
269
|
+
const availableCount = ref<number | null>(null);
|
|
241
270
|
|
|
242
|
-
async function fetchCards() {
|
|
271
|
+
async function fetchCards(type: "NFC" | "QRCODE" = "NFC") {
|
|
272
|
+
if (!props.siteId || !props.unitId) return;
|
|
243
273
|
cardsLoading.value = true;
|
|
244
274
|
try {
|
|
245
|
-
const res = await
|
|
246
|
-
|
|
275
|
+
const res = await getAvailableContractorCards({
|
|
276
|
+
type,
|
|
277
|
+
siteId: props.siteId,
|
|
278
|
+
unitId: props.unitId,
|
|
279
|
+
});
|
|
280
|
+
cardItems.value = res?.data?.[0]?.items ?? [];
|
|
281
|
+
availableCount.value = (res?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
282
|
+
|
|
283
|
+
if (type === "QRCODE" && props.settings?.data?._id && availableCount.value !== null && availableCount.value < 50) {
|
|
284
|
+
await generateQrVms({
|
|
285
|
+
site: props.settings.data.site,
|
|
286
|
+
unitId: props.unitId,
|
|
287
|
+
quantity: 50,
|
|
288
|
+
});
|
|
289
|
+
// Re-fetch to get the updated count after generation
|
|
290
|
+
const refreshed = await getAvailableContractorCards({
|
|
291
|
+
type,
|
|
292
|
+
siteId: props.siteId,
|
|
293
|
+
unitId: props.unitId,
|
|
294
|
+
});
|
|
295
|
+
cardItems.value = refreshed?.data?.[0]?.items ?? [];
|
|
296
|
+
availableCount.value = (refreshed?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
297
|
+
}
|
|
298
|
+
} catch (err: any) {
|
|
299
|
+
const msg = err?.data?.message ?? err?.message ?? String(err);
|
|
300
|
+
showSnackbar(`Entry pass service error: ${msg}`);
|
|
301
|
+
availableCount.value = null;
|
|
302
|
+
cardItems.value = [];
|
|
247
303
|
} finally {
|
|
248
304
|
cardsLoading.value = false;
|
|
249
305
|
}
|
|
@@ -268,7 +324,7 @@ const toggleBarcodeScanning = () => {
|
|
|
268
324
|
};
|
|
269
325
|
|
|
270
326
|
const validateScannedBarcode = (scannedCode: string) => {
|
|
271
|
-
const card = cardItems.value.find((c) => c.
|
|
327
|
+
const card = cardItems.value.find((c) => c.cardNo === scannedCode);
|
|
272
328
|
if (card) addCard(card);
|
|
273
329
|
barcodeBuffer.value = "";
|
|
274
330
|
};
|
|
@@ -360,7 +416,7 @@ const scanFrame = async () => {
|
|
|
360
416
|
try {
|
|
361
417
|
const barcodes = await barcodeDetector.detect(canvas);
|
|
362
418
|
for (const barcode of barcodes) {
|
|
363
|
-
const card = cardItems.value.find((c) => c.
|
|
419
|
+
const card = cardItems.value.find((c) => c.cardNo === barcode.rawValue);
|
|
364
420
|
if (card) {
|
|
365
421
|
addCard(card);
|
|
366
422
|
closeCameraDialog();
|
|
@@ -390,9 +446,13 @@ watch(
|
|
|
390
446
|
() => passType.value,
|
|
391
447
|
(val) => {
|
|
392
448
|
if (val === "NFC") {
|
|
393
|
-
fetchCards();
|
|
449
|
+
fetchCards("NFC");
|
|
394
450
|
window.addEventListener("keydown", handleBarcodeInput);
|
|
451
|
+
} else if (val === "QR") {
|
|
452
|
+
availableCount.value = null;
|
|
453
|
+
fetchCards("QRCODE");
|
|
395
454
|
} else {
|
|
455
|
+
availableCount.value = null;
|
|
396
456
|
isBarcodeScanningActive.value = false;
|
|
397
457
|
barcodeBuffer.value = "";
|
|
398
458
|
if (barcodeTimeout.value) clearTimeout(barcodeTimeout.value);
|
|
@@ -283,7 +283,9 @@
|
|
|
283
283
|
>mdi-delete-outline</v-icon
|
|
284
284
|
>
|
|
285
285
|
</template>
|
|
286
|
-
<v-list-item-title
|
|
286
|
+
<v-list-item-title
|
|
287
|
+
>Remove Equipment</v-list-item-title
|
|
288
|
+
>
|
|
287
289
|
</v-list-item>
|
|
288
290
|
</v-list>
|
|
289
291
|
</v-menu>
|
|
@@ -440,7 +442,8 @@
|
|
|
440
442
|
<script setup lang="ts">
|
|
441
443
|
import useEquipmentItem from "../composables/useEquipmentItem";
|
|
442
444
|
import { useEquipmentItemPermission } from "../composables/useEquipmentItemPermission";
|
|
443
|
-
import
|
|
445
|
+
import useEquipmentManagement from "../composables/useEquipmentManagement";
|
|
446
|
+
import useFile from "../composables/useFile";
|
|
444
447
|
import useUtils from "../composables/useUtils";
|
|
445
448
|
|
|
446
449
|
const props = defineProps({
|
|
@@ -546,7 +549,7 @@ watchEffect(() => {
|
|
|
546
549
|
}
|
|
547
550
|
});
|
|
548
551
|
|
|
549
|
-
const { getSupplies } =
|
|
552
|
+
const { getSupplies } = useEquipmentManagement();
|
|
550
553
|
|
|
551
554
|
const { data: getSuppliesReq } = await useLazyAsyncData(
|
|
552
555
|
"get-supplies-for-equipment-item",
|
|
@@ -568,7 +571,9 @@ watchEffect(() => {
|
|
|
568
571
|
}
|
|
569
572
|
});
|
|
570
573
|
|
|
571
|
-
const supplyItems = ref<
|
|
574
|
+
const supplyItems = ref<
|
|
575
|
+
Array<{ name: string; value: string; stockQty: number }>
|
|
576
|
+
>([]);
|
|
572
577
|
const selectedSupplyForAdd = ref<string>("");
|
|
573
578
|
const equipmentItems = ref<TEquipmentItem[]>([]);
|
|
574
579
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<template #label> Subject <span class="text-red">*</span> </template>
|
|
32
32
|
</v-autocomplete>
|
|
33
33
|
|
|
34
|
-
<v-autocomplete
|
|
34
|
+
<!-- <v-autocomplete
|
|
35
35
|
v-model="localFeedback.category"
|
|
36
36
|
:items="categories"
|
|
37
37
|
item-title="title"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
:rules="[requiredRule]"
|
|
46
46
|
>
|
|
47
47
|
<template #label> Category <span class="text-red">*</span> </template>
|
|
48
|
-
</v-autocomplete>
|
|
48
|
+
</v-autocomplete> -->
|
|
49
49
|
|
|
50
50
|
<v-text-field
|
|
51
51
|
label="Location"
|
|
@@ -91,7 +91,7 @@ const props = defineProps<{
|
|
|
91
91
|
feedback: TFeedbackCreate;
|
|
92
92
|
isEditMode: boolean;
|
|
93
93
|
loading: boolean;
|
|
94
|
-
categories: { title: string; value: string }[];
|
|
94
|
+
// categories: { title: string; value: string }[];
|
|
95
95
|
erroredImages?: string[];
|
|
96
96
|
maxFiles?: number;
|
|
97
97
|
}>();
|
|
@@ -133,7 +133,7 @@ const { requiredRule } = useUtils();
|
|
|
133
133
|
const isFormValid = computed(() => {
|
|
134
134
|
return (
|
|
135
135
|
localFeedback.value.subject &&
|
|
136
|
-
localFeedback.value.category &&
|
|
136
|
+
// localFeedback.value.category &&
|
|
137
137
|
localFeedback.value.location &&
|
|
138
138
|
localFeedback.value.description
|
|
139
139
|
);
|