Skip to content

Commit de27103

Browse files
committed
feat(admin-borrow-page): scan device first, then scan id cart
1 parent 161ba63 commit de27103

15 files changed

Lines changed: 342 additions & 125 deletions

File tree

components/app/Checkout/DeviceSearchBox.vue

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const emits = defineEmits<{
55
'device-select': [string],
66
}>();
77
8+
const { lab } = useLab();
9+
810
const searchText = ref('');
911
const { isActive: isDropdownActive, setInactive } = useClick(useTemplateRef('dropdown'));
1012
@@ -17,7 +19,7 @@ watch(searchText, async () => {
1719
searchItems.value = [];
1820
return;
1921
}
20-
const data = await deviceKindService.getDeviceKinds(0, numberOfSearchItemsShown, { searchText: searchText.value || undefined, searchFields: ['device_name', 'device_id'] });
22+
const data = await deviceKindService.getDeviceKindsByLabId(lab.value.id, 0, numberOfSearchItemsShown, { searchText: searchText.value || undefined, searchFields: ['device_name', 'device_id'] });
2123
searchItems.value = data.deviceKinds.map(({ name, mainImage, id }) => ({ id, name, image: mainImage }));
2224
});
2325
function focusNextSearchItem () {
@@ -41,18 +43,26 @@ function unfocusSearchItem () {
4143
<div ref="dropdown" class="relative">
4244
<div class="relative">
4345
<input
44-
v-model="searchText"
46+
v-model="searchText"
4547
class="bg-white text-primary-light placeholder:text-primary-light border-2 h-11 w-[100%] pl-10 pr-3 rounded-md text-md placeholder:text-normal"
46-
type="search" placeholder="Tên/Mã loại thiết bị" @keydown.down="focusNextSearchItem" @keydown.up="focusPrevSearchItem" @keydown.enter="focusedSearchItemIndex !== null && goToSearchItem(searchItems[focusedSearchItemIndex!].id)" @keydown.esc="unfocusSearchItem">
48+
type="search" placeholder="Tên/Mã loại thiết bị" @keydown.down="focusNextSearchItem"
49+
@keydown.up="focusPrevSearchItem"
50+
@keydown.enter="focusedSearchItemIndex !== null && goToSearchItem(searchItems[focusedSearchItemIndex!].id)"
51+
@keydown.esc="unfocusSearchItem">
4752
<Icon
48-
aria-hidden class="absolute left-3 top-[12px] text-xl text-primary-dark"
53+
aria-hidden class="absolute left-3 top-[12px] text-xl text-primary-dark"
4954
name="i-heroicons-magnifying-glass" />
5055
</div>
5156

52-
<div :class="`${isDropdownActive && searchItems.length ? 'flex' : 'hidden'} flex-col gap-1 absolute bg-white p-1 mt-1 w-[120%] z-50 shadow-[0_0px_16px_-3px_rgba(0,0,0,0.3)]`">
53-
<a v-for="(item, index) in searchItems" :key="item.id" :class="`px-2 text-normal p-1 flex justify-start gap-2 hover:bg-gray-100 ${focusedSearchItemIndex === index ? 'bg-secondary-light' : ''}`" @click="goToSearchItem(searchItems[index].id)">
57+
<div
58+
:class="`${isDropdownActive && searchItems.length ? 'flex' : 'hidden'} flex-col gap-1 absolute bg-white p-1 mt-1 w-[120%] z-50 shadow-[0_0px_16px_-3px_rgba(0,0,0,0.3)]`">
59+
<a
60+
v-for="(item, index) in searchItems" :key="item.id"
61+
:class="`px-2 text-normal p-1 flex justify-start gap-2 hover:bg-gray-100 ${focusedSearchItemIndex === index ? 'bg-secondary-light' : ''}`"
62+
@click="goToSearchItem(searchItems[index].id)">
5463
<img :src="item.image" class="h-6 w-6 block">
55-
<p class="p-1 px-2 text-nowrap bg-gray-100 border border-gray-300 rounded-md text-normal font-normal leading-none">
64+
<p
65+
class="p-1 px-2 text-nowrap bg-gray-100 border border-gray-300 rounded-md text-normal font-normal leading-none">
5666
{{ item.id.toUpperCase() }}
5767
</p>
5868
<HighlightText class="line-clamp-1" :text="item.name" :match-text="searchText || undefined" />

components/app/Checkout/DeviceSelectModal.vue

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const props = defineProps<{
77
selectedDevices: string[];
88
}>();
99
10+
const { lab } = useLab();
11+
1012
const emits = defineEmits<{
1113
'close-modal': [],
1214
'device-add': [{ kind: string, id: string }],
@@ -16,24 +18,31 @@ const emits = defineEmits<{
1618
const deviceKindMeta = ref<null | DeviceKindResourceDto>(null);
1719
watch(() => [props.kindId], async () => {
1820
if (props.kindId) {
19-
deviceKindMeta.value = await deviceKindService.getById(props.kindId);
21+
deviceKindMeta.value = await deviceKindService.getById(props.kindId, lab.value.id);
2022
return;
2123
}
2224
deviceKindMeta.value = null;
2325
});
2426
</script>
2527

2628
<template>
27-
<div v-if="deviceKindMeta && kindId" class="fixed z-50 top-0 left-0 w-[100vw] h-[100vh] flex justify-center items-center">
29+
<div
30+
v-if="deviceKindMeta && kindId"
31+
class="fixed z-50 top-0 left-0 w-[100vw] h-[100vh] flex justify-center items-center">
2832
<div class="bg-white shadow-[0_0px_16px_1px_rgba(0,0,0,0.3)] w-[400px] sm:w-[90vw] p-6">
2933
<div class="flex justify-between gap-3 items-center">
3034
<h2 class="text-lg"> {{ deviceKindMeta.name }} </h2>
31-
<div class="border border-slate text-slate rounded-full flex justify-center items-center p-2 hover:cursor-pointer" @click="emits('close-modal')">
35+
<div
36+
class="border border-slate text-slate rounded-full flex justify-center items-center p-2 hover:cursor-pointer"
37+
@click="emits('close-modal')">
3238
<Icon class="text-lg" aria-hidden name="i-heroicons-x-mark" />
3339
</div>
3440
</div>
3541
<div class="mt-6">
36-
<CheckoutDeviceSelectTable :selected-devices="selectedDevices" :kind-id="kindId" @device-add="(id) => emits('device-add', { kind: props.kindId!, id })" @device-delete="(id) => emits('device-delete', { kind: props.kindId!, id})" />
42+
<CheckoutDeviceSelectTable
43+
:selected-devices="selectedDevices" :kind-id="kindId"
44+
@device-add="(id) => emits('device-add', { kind: props.kindId!, id })"
45+
@device-delete="(id) => emits('device-delete', { kind: props.kindId!, id })" />
3746
</div>
3847
</div>
3948
</div>

components/app/Checkout/DeviceSelectTable/index.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ const emits = defineEmits<{
1313
'device-delete': [string],
1414
}>();
1515
16+
const { lab } = useLab();
17+
1618
async function fetchData (offset: number, length: number, options: { desc?: boolean, sortField?: string, searchText?: string, searchFields?: string[] }): Promise<{ data: unknown[], totalPages: number }> {
17-
const res = await deviceService.getByKind(props.kindId, offset, length, { searchText: options.searchText, searchFields: ['device_id'], sortField: options.sortField as any, desc: options.desc });
19+
const res = await deviceService.getByKind(props.kindId, offset, length, { searchText: options.searchText, searchFields: ['device_id'], sortField: options.sortField as any, desc: options.desc }, lab.value.id);
1820
return {
1921
data: res.devices,
2022
totalPages: res.totalPages,
@@ -31,5 +33,7 @@ async function borrowDevice (id: string) {
3133
</script>
3234

3335
<template>
34-
<DataTable :selectable="false" :searchable="true" :qrable="true" :fetch-fn="fetchData" :columns="createColumns(props.selectedDevices, { deleteDevice, borrowDevice }) as AugmentedColumnDef<unknown>[]" />
36+
<DataTable
37+
:selectable="false" :searchable="true" :qrable="true" :fetch-fn="fetchData"
38+
:columns="createColumns(props.selectedDevices, { deleteDevice, borrowDevice }) as AugmentedColumnDef<unknown>[]" />
3539
</template>

components/app/LabAdminTable/column.ts

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,110 @@
1-
import type { LabManagedByAdmin } from "./schema";
2-
import type { AugmentedColumnDef } from "~/components/common/DataTable/column";
3-
import { Icon, Button } from "#components";
1+
import type { LabManagedByAdmin } from './schema';
2+
import type { AugmentedColumnDef } from '~/components/common/DataTable/column';
3+
import { Icon, Button } from '#components';
44

55
const formatTimetable = (timetable: Record<string, string[]>) => {
66
const days: Record<string, string> = {
7-
"2": "Thứ 2",
8-
"3": "Thứ 3",
9-
"4": "Thứ 4",
10-
"5": "Thứ 5",
11-
"6": "Thứ 6",
12-
"7": "Thứ 7",
13-
"8": "Chủ nhật",
7+
'2': 'Thứ 2',
8+
'3': 'Thứ 3',
9+
'4': 'Thứ 4',
10+
'5': 'Thứ 5',
11+
'6': 'Thứ 6',
12+
'7': 'Thứ 7',
13+
'8': 'Chủ nhật',
1414
};
1515

1616
return Object.entries(timetable || {}).map(([day, slots]) => {
17-
return h("div", { class: "mb-1" }, [
18-
h("span", { class: "font-medium" }, `${days[day]}: `),
19-
h("span", {}, slots.join(", ")),
17+
return h('div', { class: 'mb-1' }, [
18+
h('span', { class: 'font-medium' }, `${days[day]}: `),
19+
h('span', {}, slots.join(', ')),
2020
]);
2121
});
2222
};
2323

2424
export const columns: AugmentedColumnDef<LabManagedByAdmin>[] = [
2525
{
26-
id: "name",
27-
title: "Tên phòng",
26+
id: 'name',
27+
title: 'Tên phòng',
2828
cell: ({ row }) =>
2929
h(
30-
"div",
30+
'div',
3131
{
32-
class: "flex items-center gap-2",
32+
class: 'flex items-center gap-2',
3333
},
3434
[
3535
h(Icon, {
36-
name: "i-heroicons-beaker",
37-
class: "w-5 h-5 text-blue-500",
36+
name: 'i-heroicons-beaker',
37+
class: 'w-5 h-5 text-blue-500',
3838
}),
39-
h("span", { class: "text-gray-700 font-medium" }, row.original.name),
39+
h('span', { class: 'text-gray-700 font-medium' }, row.original.name),
4040
],
4141
),
4242
enableSorting: true,
4343
},
4444
{
45-
id: "room",
46-
title: "Phòng",
47-
cell: ({ row }) => h("span", { class: "text-gray-600" }, row.original.room),
45+
id: 'room',
46+
title: 'Phòng',
47+
cell: ({ row }) => h('span', { class: 'text-gray-600' }, row.original.room),
4848
enableSorting: true,
4949
},
5050
{
51-
id: "faculty",
52-
title: "Khoa",
51+
id: 'faculty',
52+
title: 'Khoa',
5353
cell: ({ row }) =>
54-
h("span", { class: "text-gray-600" }, row.original.faculty),
54+
h('span', { class: 'text-gray-600' }, row.original.faculty),
5555
enableSorting: true,
5656
},
5757
{
58-
id: "branch",
59-
title: "Cơ sở",
58+
id: 'branch',
59+
title: 'Cơ sở',
6060
cell: ({ row }) =>
61-
h("span", { class: "text-gray-600" }, row.original.branch),
61+
h('span', { class: 'text-gray-600' }, row.original.branch),
6262
enableSorting: true,
6363
},
6464
{
65-
id: "timetable",
66-
title: "Lịch hoạt động",
65+
id: 'timetable',
66+
title: 'Lịch hoạt động',
6767
cell: ({ row }) =>
6868
h(
69-
"div",
70-
{ class: "text-sm text-gray-600" },
69+
'div',
70+
{ class: 'text-sm text-gray-600' },
7171
row.original.timetable
7272
? formatTimetable(row.original.timetable)
73-
: "Chưa có lịch",
73+
: 'Chưa có lịch',
7474
),
7575
},
7676
{
77-
id: "actions",
78-
title: "Thao tác",
77+
id: 'actions',
78+
title: 'Thao tác',
7979
cell: ({ row }) =>
80-
h("div", { class: "flex items-center gap-2" }, [
80+
h('div', { class: 'flex items-center gap-2' }, [
8181
h(
8282
Button,
8383
{
84-
variant: "ghost",
85-
size: "icon",
86-
onClick: () => console.log("Edit:", row.original),
87-
class: "hover:text-blue-500",
84+
variant: 'ghost',
85+
size: 'icon',
86+
onClick: () => console.log('Edit:', row.original),
87+
class: 'hover:text-blue-500',
8888
},
8989
[
9090
h(Icon, {
91-
name: "i-heroicons-pencil-square",
92-
class: "w-5 h-5",
91+
name: 'i-heroicons-pencil-square',
92+
class: 'w-5 h-5',
9393
}),
9494
],
9595
),
9696
h(
9797
Button,
9898
{
99-
variant: "ghost",
100-
size: "icon",
101-
onClick: () => console.log("Delete:", row.original),
102-
class: "hover:text-red-500",
99+
variant: 'ghost',
100+
size: 'icon',
101+
onClick: () => console.log('Delete:', row.original),
102+
class: 'hover:text-red-500',
103103
},
104104
[
105105
h(Icon, {
106-
name: "i-heroicons-trash",
107-
class: "w-5 h-5",
106+
name: 'i-heroicons-trash',
107+
class: 'w-5 h-5',
108108
}),
109109
],
110110
),

components/app/LabAdminTable/index.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { laboratoryService } from '~/services';
33
import { columns } from './column';
44
import type { AugmentedColumnDef } from '~/components/common/DataTable/column';
55
6-
async function fetchData(offset: number, length: number, options: { desc?: boolean, sortField?: string, searchText?: string, searchFields?: string[] }): Promise<{ data: unknown[], totalPages: number }> {
6+
async function fetchData (offset: number, length: number, options: { desc?: boolean, sortField?: string, searchText?: string, searchFields?: string[] }): Promise<{ data: unknown[], totalPages: number }> {
77
const res = await laboratoryService.getLabsManagedByAdmin(offset, length, { searchText: options.searchText, searchFields: ['lab_name', 'location'], sortField: options.sortField as any, desc: options.desc });
88
return {
99
data: res.labs,
@@ -13,6 +13,7 @@ async function fetchData(offset: number, length: number, options: { desc?: boole
1313
</script>
1414

1515
<template>
16-
<DataTable :selectable="true" :searchable="true" :qrable="true" :fetch-fn="fetchData"
16+
<DataTable
17+
:selectable="true" :searchable="true" :qrable="true" :fetch-fn="fetchData"
1718
:columns="columns as AugmentedColumnDef<unknown>[]" />
1819
</template>

components/app/LabAdminTable/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Type } from "@sinclair/typebox";
2-
import type { Static } from "@sinclair/typebox";
1+
import { Type } from '@sinclair/typebox';
2+
import type { Static } from '@sinclair/typebox';
33

44
export const LabManagedByAdmin = Type.Object({
55
id: Type.String(),

composables/states.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const useLab = () => {
2+
const lab = useState<{ id: string }>('lab', () => ({
3+
id: '015bd698-f8fb-4672-a43e-4e6fa64305ea',
4+
}));
5+
6+
return {
7+
lab,
8+
};
9+
};

pages/admin/borrows/form.vue

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { deviceKindService, receiptService, userService } from '~/services';
44
55
const route = useRoute();
66
7-
87
const currentDeviceKindId = ref<string | null>(null);
98
109
const devicesInCart = ref<{
@@ -121,11 +120,39 @@ async function submitReceipt () {
121120
reloadNuxtApp();
122121
}
123122
124-
onMounted(() => {
123+
const handleVirtualKeyboardDetection = async (input: string, type?: 'userId' | 'device') => {
124+
if (type === 'userId') {
125+
userCodeInput.value = input;
126+
} else if (type === 'device') {
127+
const deviceKindId = input.match(/\/devices\/([a-fA-F0-9]+)/)?.[1];
128+
const deviceId = input.match(/[?&]id=([a-fA-F0-9]+)/)![1];
129+
const { id, status } = await deviceService.checkDevice(deviceId, lab.value.id);
130+
if (status === 'borrowing') {
131+
} else if (status === 'healthy') {
132+
}
133+
}
134+
};
135+
136+
useVirtualKeyboardDetection(handleVirtualKeyboardDetection, {
137+
userId: { length: 7 },
138+
device: { pattern: /^https?:\/\/[^/]+\/devices\/\d{8}\?id=[a-fA-F0-9]+$/ },
139+
scannerThresholdMs: 100,
140+
maxInputTimeMs: 1000,
141+
});
142+
143+
144+
onMounted(async () => {
125145
const userId = route.query.userId;
146+
const deviceKindId = route.query.deviceKindId;
147+
const deviceId = route.query.deviceId;
148+
126149
if (userId && typeof userId === 'string') {
127150
userCodeInput.value = userId;
128151
}
152+
153+
if (deviceKindId && deviceId && typeof deviceKindId === 'string' && typeof deviceId === 'string') {
154+
await addDevice({ kind: deviceKindId, id: deviceId });
155+
}
129156
});
130157
</script>
131158

pages/index.vue

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,30 @@ definePageMeta({
66
permission: 'home:own',
77
});
88
9+
const { lab } = useLab();
10+
911
const showDialog = ref(false);
1012
const userId = ref('');
11-
const deviceId = ref('');
1213
1314
const handleVirtualKeyboardDetection = async (input: string, type?: 'userId' | 'device') => {
1415
if (type === 'userId') {
1516
showDialog.value = true;
1617
userId.value = input;
1718
} else if (type === 'device') {
18-
deviceId.value = input;
19-
const { id } = await deviceService.checkDevice(input.match(/[?&]id=([a-fA-F0-9]+)/)![1]);
20-
navigateTo(`/device/${id}`);
19+
const deviceKindId = input.match(/\/devices\/([a-fA-F0-9]+)/)?.[1];
20+
const deviceId = input.match(/[?&]id=([a-fA-F0-9]+)/)![1];
21+
const { id, status } = await deviceService.checkDevice(deviceId, lab.value.id);
22+
if (status === 'borrowing') {
23+
navigateTo({
24+
path: '/admin/returns/form',
25+
query: { deviceKindId, deviceId }
26+
});
27+
} else if (status === 'healthy') {
28+
navigateTo({
29+
path: '/admin/borrows/form',
30+
query: { deviceKindId, deviceId }
31+
});
32+
}
2133
}
2234
};
2335

0 commit comments

Comments
 (0)