Skip to content

Commit 38160ca

Browse files
committed
Merge branch 'feat/add-device-page' of github.com:LabSyncro/LabSyncro into feat/devops
2 parents 5130e95 + 4134a5e commit 38160ca

34 files changed

Lines changed: 1193 additions & 266 deletions

File tree

components/app/Checkout/DeviceKindTable/column.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,51 @@ import type { AugmentedColumnDef } from '~/components/common/DataTable/column';
55
export function createColumns ({
66
onDeviceKindLinkClick,
77
}: {
8-
onDeviceKindLinkClick: (id: string) => void,
8+
onDeviceKindLinkClick: (id: string) => void;
99
}): AugmentedColumnDef<DeviceKindInCartList>[] {
1010
return [
1111
{
1212
id: 'id',
1313
title: 'Mã loại thiết bị',
1414
cell: ({ row }) =>
15-
h(
16-
'p',
17-
{ class: 'text-normal pl-3' },
18-
[row.original.id.toUpperCase()],
19-
),
15+
h('p', { class: 'text-normal pl-3' }, [row.original.id.toUpperCase()]),
2016
},
2117
{
2218
id: 'name',
2319
title: 'Tên loại thiết bị',
2420
cell: ({ row }) =>
2521
h(
2622
'p',
27-
{ class: 'line-clamp-2 text-slate-500 text-normal leading-6 font-normal' },
23+
{
24+
class:
25+
'line-clamp-2 text-slate-500 text-normal leading-6 font-normal',
26+
},
2827
row.original.name,
2928
),
3029
},
30+
{
31+
id: 'category',
32+
title: 'Phân nhóm',
33+
cell: ({ row }) =>
34+
h(
35+
'p',
36+
{
37+
class:
38+
'line-clamp-2 text-slate-500 text-normal leading-6 font-normal',
39+
},
40+
row.original.category,
41+
),
42+
},
3143
{
3244
id: 'quantity',
3345
title: 'SL',
3446
cell: ({ row }) =>
3547
h(
3648
'p',
37-
{ class: 'line-clamp-2 text-slate-500 text-right text-normal leading-6 font-normal' },
49+
{
50+
class:
51+
'line-clamp-2 text-slate-500 text-center text-normal leading-6 font-normal',
52+
},
3853
row.original.quantity,
3954
),
4055
},
@@ -44,7 +59,10 @@ export function createColumns ({
4459
cell: ({ row }) =>
4560
h(
4661
'div',
47-
{ class: 'flex justify-end items-center text-lg hover:cursor-pointer', onClick: () => onDeviceKindLinkClick(row.original.id) },
62+
{
63+
class: 'flex justify-end items-center text-lg hover:cursor-pointer',
64+
onClick: () => onDeviceKindLinkClick(row.original.id),
65+
},
4866
h(Icon, { name: 'i-heroicons-arrow-top-right-on-square' }),
4967
),
5068
},

components/app/Checkout/DeviceKindTable/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const props = defineProps<{
77
cart: {
88
id: string;
99
name: string;
10+
category: string;
1011
deviceIds: string[];
1112
}[],
1213
}>();
@@ -28,6 +29,7 @@ async function fetchData (offset: number, length: number, options: { desc?: bool
2829
let deviceKinds = props.cart.map((deviceKind) => ({
2930
id: deviceKind.id,
3031
name: deviceKind.name,
32+
category: deviceKind.category,
3133
quantity: deviceKind.deviceIds.length,
3234
}));
3335
if (options.sortField) {
@@ -45,5 +47,8 @@ async function fetchData (offset: number, length: number, options: { desc?: bool
4547
</script>
4648

4749
<template>
48-
<DataTable :key="cart.flatMap(({ deviceIds }) => deviceIds).join('-')" :selectable="false" :searchable="false" :qrable="false" :fetch-fn="fetchData" :delete-fn="deleteData" :columns="createColumns({ onDeviceKindLinkClick }) as AugmentedColumnDef<unknown>[]" />
50+
<DataTable
51+
:key="cart.flatMap(({ deviceIds }) => deviceIds).join('-')" :selectable="false" :searchable="false"
52+
:qrable="false" :fetch-fn="fetchData" :delete-fn="deleteData"
53+
:columns="createColumns({ onDeviceKindLinkClick }) as AugmentedColumnDef<unknown>[]" />
4954
</template>

components/app/Checkout/DeviceKindTable/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Static } from '@sinclair/typebox';
44
export const DeviceKindInCartList = Type.Object({
55
id: Type.String(),
66
name: Type.String(),
7+
category: Type.String(),
78
quantity: Type.Number(),
89
});
910

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/DeviceNew/PrintQRCode.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const generateDevices = async (): Promise<{ id: string; url: string; name: strin
2626
return props.listDeviceIds.map(deviceId => {
2727
return {
2828
id: `${deviceKindId.toLowerCase()}/${deviceId}`,
29-
url: `http://localhost:3000/devices/${deviceKindId.toLowerCase()}`,
29+
url: `http://localhost:3000/devices/${deviceKindId.toLowerCase()}?id=${deviceId}`,
3030
name: deviceKindName,
3131
};
3232
});

components/app/DeviceTable/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { deviceKindService } from '~/services';
33
import { columns } from './column';
44
import type { AugmentedColumnDef } from '~/components/common/DataTable/column';
55
6+
const router = useRouter();
7+
68
async function deleteData (ids: string[]) {
79
await deviceKindService.deleteByIds(ids);
810
}
@@ -17,5 +19,8 @@ async function fetchData (offset: number, length: number, options: { desc?: bool
1719
</script>
1820

1921
<template>
20-
<DataTable :selectable="true" :searchable="true" :qrable="true" :add-trigger-fn="() => {}" :fetch-fn="fetchData" :delete-fn="deleteData" :columns="columns as AugmentedColumnDef<unknown>[]" />
22+
<DataTable
23+
:selectable="true" :searchable="true" :qrable="true"
24+
:add-trigger-fn="() => { router.push('/admin/devices/new') }" :fetch-fn="fetchData" :delete-fn="deleteData"
25+
:columns="columns as AugmentedColumnDef<unknown>[]" />
2126
</template>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { LabManagedByAdmin } from './schema';
2+
import type { AugmentedColumnDef } from '~/components/common/DataTable/column';
3+
import { Icon, Button } from '#components';
4+
5+
const formatTimetable = (timetable: Record<string, string[]>) => {
6+
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',
14+
};
15+
16+
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(', ')),
20+
]);
21+
});
22+
};
23+
24+
export const columns: AugmentedColumnDef<LabManagedByAdmin>[] = [
25+
{
26+
id: 'name',
27+
title: 'Tên phòng',
28+
cell: ({ row }) =>
29+
h(
30+
'div',
31+
{
32+
class: 'flex items-center gap-2',
33+
},
34+
[
35+
h(Icon, {
36+
name: 'i-heroicons-beaker',
37+
class: 'w-5 h-5 text-blue-500',
38+
}),
39+
h('span', { class: 'text-gray-700 font-medium' }, row.original.name),
40+
],
41+
),
42+
enableSorting: true,
43+
},
44+
{
45+
id: 'room',
46+
title: 'Phòng',
47+
cell: ({ row }) => h('span', { class: 'text-gray-600' }, row.original.room),
48+
enableSorting: true,
49+
},
50+
{
51+
id: 'faculty',
52+
title: 'Khoa',
53+
cell: ({ row }) =>
54+
h('span', { class: 'text-gray-600' }, row.original.faculty),
55+
enableSorting: true,
56+
},
57+
{
58+
id: 'branch',
59+
title: 'Cơ sở',
60+
cell: ({ row }) =>
61+
h('span', { class: 'text-gray-600' }, row.original.branch),
62+
enableSorting: true,
63+
},
64+
{
65+
id: 'timetable',
66+
title: 'Lịch hoạt động',
67+
cell: ({ row }) =>
68+
h(
69+
'div',
70+
{ class: 'text-sm text-gray-600' },
71+
row.original.timetable
72+
? formatTimetable(row.original.timetable)
73+
: 'Chưa có lịch',
74+
),
75+
},
76+
{
77+
id: 'actions',
78+
title: 'Thao tác',
79+
cell: ({ row }) =>
80+
h('div', { class: 'flex items-center gap-2' }, [
81+
h(
82+
Button,
83+
{
84+
variant: 'ghost',
85+
size: 'icon',
86+
onClick: () => console.log('Edit:', row.original),
87+
class: 'hover:text-blue-500',
88+
},
89+
[
90+
h(Icon, {
91+
name: 'i-heroicons-pencil-square',
92+
class: 'w-5 h-5',
93+
}),
94+
],
95+
),
96+
h(
97+
Button,
98+
{
99+
variant: 'ghost',
100+
size: 'icon',
101+
onClick: () => console.log('Delete:', row.original),
102+
class: 'hover:text-red-500',
103+
},
104+
[
105+
h(Icon, {
106+
name: 'i-heroicons-trash',
107+
class: 'w-5 h-5',
108+
}),
109+
],
110+
),
111+
]),
112+
},
113+
];
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
import { laboratoryService } from '~/services';
3+
import { columns } from './column';
4+
import type { AugmentedColumnDef } from '~/components/common/DataTable/column';
5+
6+
async function fetchData (offset: number, length: number, options: { desc?: boolean, sortField?: string, searchText?: string, searchFields?: string[] }): Promise<{ data: unknown[], totalPages: number }> {
7+
const res = await laboratoryService.getLabsManagedByAdmin(offset, length, { searchText: options.searchText, searchFields: ['lab_name', 'location'], sortField: options.sortField as any, desc: options.desc });
8+
return {
9+
data: res.labs,
10+
totalPages: res.totalPages,
11+
};
12+
}
13+
</script>
14+
15+
<template>
16+
<DataTable
17+
:selectable="true" :searchable="true" :qrable="true" :fetch-fn="fetchData"
18+
:columns="columns as AugmentedColumnDef<unknown>[]" />
19+
</template>

0 commit comments

Comments
 (0)