This commit is contained in:
Viet An
2026-04-14 22:44:52 +07:00
parent 1d30ca3408
commit e5b80124fc
23 changed files with 1045 additions and 158 deletions

View File

@@ -129,18 +129,12 @@ $size: (
// Block Layout
.blockdiv {
max-width: 1900px !important;
padding: 60px 15px 40px 15px;
@include until($desktop) { padding: 65px 20px 30px 20px; }
@include mobile { padding: 65px 16px 30px 16px; }
padding: 1rem 2rem 2rem;
@include mobile { padding: 1rem; }
.columns .column {
@include mobile { padding-left: 0; padding-right: 0; }
}
.padding-desktop {
@media screen and (min-width: $desktop) { padding-left: 20px; padding-right: 20px; }
}
}
// Tooltip Styles

View File

@@ -1,5 +1,5 @@
<template>
<nav class="navbar is-fixed-top has-shadow px-3" role="navigation">
<nav class="navbar has-shadow sticky px-3" style="top: 0" role="navigation">
<div class="navbar-brand mr-5">
<span class="navbar-item is-gap-1">
<div style="width: 16px; height: 16px" class="has-background-primary rounded-full"></div>
@@ -31,7 +31,7 @@
</a>
</div>
<div class="navbar-menu" id="navMenu">
<div class="navbar-start is-gap-1 is-align-items-center" style="min-width: 650px;">
<div class="navbar-start is-gap-1 is-align-items-center" >
<template v-for="(v, i) in leftmenu" :key="i" :id="v.code">
<a class="navbar-item rounded-lg is-clipped p-0" v-if="!v.submenu" @click="changeTab(v)">
<span :class="[
@@ -67,7 +67,7 @@
</div>
</template>
</div>
<div class="navbar-end">
<div v-if="false" class="navbar-end">
<a class="navbar-item" @click="changeTab(tabConfig)" v-if="tabConfig">
<SvgIcon v-bind="{ name: 'configuration.svg', type: 'findata', size: 24 }"></SvgIcon>
</a>
@@ -127,9 +127,9 @@ const menu = [
index: 0,
},
]
if($store.rights.length>0) {
menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
}
// if($store.rights.length>0) {
// menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
// }
if(menu.length===0) {
$snackbar($store.lang==='vi'? 'Bạn không có quyền truy cập' : 'You do not have permission to access.')
}

View File

@@ -68,11 +68,12 @@ const highlights = [
<div class="grid">
<DashboardHighlightCard
v-for="highlight in highlights"
:key="highlight.name"
v-bind="highlight"
/>
</div>
</div>
<div class="fixed-grid has-3-cols">
<div class="fixed-grid has-1-cols-mobile has-3-cols">
<div class="grid">
<div class="cell is-col-span-2">
<RevenueChart />

View File

@@ -1,4 +1,5 @@
<script setup>
import DeliveryInteractive from '@/components/dashboard/DeliveryInteractive.vue';
import Driver from '@/components/dashboard/Driver.vue';
const drivers = [
@@ -32,11 +33,11 @@ const drivers = [
<div class="card">
<div class="card-content">
<p class="fs-17 font-semibold mb-4">Giao nhận & Tài xế</p>
<div class="fixed-grid has-3-cols">
<div class="fixed-grid has-1-cols-mobile has-3-cols">
<div class="grid">
<div class="cell is-col-span-2">
<div style="border-radius: 0.5rem; overflow: hidden">
<NuxtImg src="/map-demo.png" class="w-full" />
<div>
<DeliveryInteractive />
</div>
</div>
<div class="cell is-flex is-flex-direction-column is-gap-2">

View File

@@ -0,0 +1,477 @@
<template>
<div
class="relative w-full has-background-blue-95 rounded-lg is-clipped"
style="height: 360px"
>
<div class="absolute inset-0 w-full h-full opacity-20"><div class="absolute w-full border-t border-gray-300" style="top: 20%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 40%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 60%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 80%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 100%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 20%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 40%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 60%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 80%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 100%;"></div>
</div>
<div>
<div
class="absolute transform -translate-x-1/2 -translate-y-full"
style="left: 33.3333%; top: 40%"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-map-pin w-6 h-6 has-text-orange-50"
>
<path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-full"
style="left: 56.6667%; top: 60%"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-map-pin w-6 h-6 has-text-green-50"
>
<path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-full"
style="left: 10%; top: 28%"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-map-pin w-6 h-6 has-text-orange-50"
>
<path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-full"
style="left: 83.3333%; top: 80%"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-map-pin w-6 h-6 has-text-green-50"
>
<path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-full"
style="left: 40%; top: 52%"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-map-pin w-6 h-6 has-text-blue-50"
>
<path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-1/2 transition-all duration-3000 ease-linear animate-move-random-1"
style="left: 39.3933%; top: 56.171%"
>
<div class="relative">
<div
class="w-10 h-10 rounded-full has-background-blue-50 border-2 border-white is-flex is-justify-content-center is-align-items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="white"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-navigation w-5 h-5 text-white"
>
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg>
</div>
<div
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
</div>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-1/2 transition-all duration-3000 ease-linear animate-move-random-2"
style="left: 22.8854%; top: 33.4304%"
>
<div class="relative">
<div
class="w-10 h-10 rounded-full has-background-blue-50 border-2 border-white is-flex is-justify-content-center is-align-items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="white"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-navigation w-5 h-5 text-white"
>
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg>
</div>
<div
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
</div>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-1/2 transition-all duration-3000 ease-linear"
style="left: 73.3333%; top: 72%"
>
<div class="relative">
<div
class="w-10 h-10 rounded-full bg-gray-400 border-2 border-white is-flex is-justify-content-center is-align-items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="white"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-navigation w-5 h-5 text-white"
>
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg>
</div>
</div>
</div>
<div
class="absolute transform -translate-x-1/2 -translate-y-1/2 transition-all duration-3000 ease-linear animate-move-random-3"
style="left: 37.052%; top: 75.6278%"
>
<div class="relative">
<div
class="w-10 h-10 rounded-full has-background-blue-50 border-2 border-white is-flex is-justify-content-center is-align-items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="white"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-navigation w-5 h-5 text-white"
>
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg>
</div>
<div
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
</div>
</div>
<div
class="absolute bottom-3 right-3 backdrop-blur px-3 py-1.5 rounded-lg text-xs font-medium has-text-gray-60"
style="background-color: hsla(0, 100%, 100%, 0.9)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-map-pin w-4 h-4 inline mr-1"
>
<path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path>
<circle cx="12" cy="10" r="3"></circle></svg> Nội
</div>
</div>
</div>
</template>
<style scoped>
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.-inset-1 {
inset: calc(var(--spacing) * -1);
}
.right-3 {
right: calc(var(--spacing) * 3);
}
.bottom-3 {
bottom: calc(var(--spacing) * 3);
}
.mr-1 {
margin-right: calc(var(--spacing) * 1);
}
.inline {
display: inline;
}
.h-3 {
height: calc(var(--spacing) * 3);
}
.h-5 {
height: calc(var(--spacing) * 5);
}
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.w-3 {
width: calc(var(--spacing) * 3);
}
.w-5 {
width: calc(var(--spacing) * 5);
}
.w-6 {
width: calc(var(--spacing) * 6);
}
.w-10 {
width: calc(var(--spacing) * 10);
}
.w-full {
width: 100%;
}
.-translate-x-1\/2 {
--tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-full {
--tw-translate-y: -100%;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.animate-ping {
animation: ping 1s cubic-bezier(0,0,0.2,1) infinite;
}
@keyframes ping {
0% {
opacity: 0.4;
transform: scale(0.5);
}
100% {
opacity: 0;
transform: scale(1.5);
}
}
.animate-move-random-1 {
animation: move-random-1 30s ease-in-out infinite;
}
.animate-move-random-2 {
animation: move-random-2 60s ease-in-out infinite;
}
.animate-move-random-3 {
animation: move-random-3 50s ease-in-out infinite;
}
@keyframes move-random-1 {
0% {
transform: translateX(0) translateY(0)
}
33% {
transform: translateX(30px) translateY(24px)
}
66% {
transform: translateX(60px) translateY(12px)
}
100% {
transform: translateX(0) translateY(0)
}
}
@keyframes move-random-2 {
0% {
transform: translateX(0) translateY(0)
}
23% {
transform: translateX(-20px) translateY(-36px)
}
46% {
transform: translateX(0) translateY(22px)
}
75% {
transform: translateX(30px) translateY(-12px)
}
100% {
transform: translateX(0) translateY(0)
}
}
@keyframes move-random-3 {
0% {
transform: translateX(0) translateY(0)
}
10% {
transform: translateX(-30px) translateY(-15px)
}
30% {
transform: translateX(-10px) translateY(26px)
}
50% {
transform: translateX(48px) translateY(-28px)
}
80% {
transform: translateX(17px) translateY(10px)
}
100% {
transform: translateX(0) translateY(0)
}
}
.border-2 {
border-style: var(--tw-border-style);
border-width: 2px;
}
.border-white {
border-color: var(--color-white);
}
.bg-gray-400 {
background-color: var(--color-gray-400);
}
.bg-white\/90 {
background-color: color-mix(in srgb, #fff 90%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 90%, transparent);
}
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
.py-1\.5 {
padding-block: calc(var(--spacing) * 1.5);
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.text-gray-600 {
color: var(--color-gray-600);
}
.text-white {
color: var(--color-white);
}
.opacity-40 {
opacity: 40%;
}
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.backdrop-blur {
--tw-backdrop-blur: blur(8px);
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
}
.transition-all {
transition-property: all;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-3000 {
--tw-duration: 3000ms;
transition-duration: 3000ms;
}
.ease-linear {
--tw-ease: linear;
transition-timing-function: linear;
}
.border-t {
border-top-style: solid;
border-top-width: 1px;
}
.border-l {
border-left-style: solid;
border-left-width: 1px;
}
.border-gray-300 {
border-color: var(--bulma-grey-85);
}
</style>

View File

@@ -117,7 +117,6 @@
weeks.value.map(v=>{
v.dates = dates.filter(x=>x.week===v.week)
})
console.log('weeks.value', weeks.value)
}
function nextMonth() {
month = month + 1

View File

@@ -33,6 +33,32 @@ const inventoryHighlights = [
</script>
<template>
<div>
<div class="content buttons is-justify-content-flex-end">
<button class="button fs-14">
<span class="icon">
<Icon :size="18" name="material-symbols:download-rounded" />
</span>
<span>Export</span>
</button>
<button class="button fs-14">
<span class="icon">
<Icon :size="18" name="material-symbols:upload-rounded" />
</span>
<span>Import</span>
</button>
<button class="button fs-14">
<span class="icon">
<Icon :size="18" name="material-symbols:sync" />
</span>
<span>Chuyển kho</span>
</button>
<button class="button fs-14 is-primary">
<span class="icon">
<Icon :size="18" name="material-symbols:add-2-rounded" />
</span>
<span>Điều chỉnh</span>
</button>
</div>
<div class="fixed-grid has-2-cols-mobile has-4-cols">
<div class="grid">
<InventoryHighlightCard

View File

@@ -13,7 +13,7 @@ const emit = defineEmits('unselect');
<button
@click="emit('unselect')"
class="button is-white rounded-full has-text-grey absolute size-8 is-flex is-justify-content-center is-align-items-center"
style="right: 0.5rem; top: 0.5rem;"
style="z-index: 1; right: 0.5rem; top: 0.5rem;"
>
<span class="icon">
<Icon name="material-symbols:close-rounded" :size="22" />

View File

@@ -0,0 +1,19 @@
<script setup>
const props = defineProps({
order: Object
});
</script>
<template>
<div class="is-flex is-flex-direction-column is-gap-2">
<div class="p-4 rounded-md has-background-primary-95">
<p class="has-text-grey">Trạng thái</p>
<p class="fs-17 mt-1 font-semibold has-text-primary-50">{{ order.delivery_status__name }}</p>
</div>
<div class="p-4 rounded-md has-background-grey-95">
<p class="has-text-grey">Địa chỉ giao hàng</p>
<p class="mt-1 has-text-grey-10">{{ order.customer__name }}</p>
<p class="has-text-grey-10">{{ order.customer__phone }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script setup>
const props = defineProps({
order: Object
});
const { $dayjs } = useNuxtApp();
const historyItems = [
{
id: 1,
name: 'Tạo đơn hàng',
details: 'Đơn hàng được tạo',
icon: 'material-symbols:info-outline-rounded',
color: 'blue',
time: '2026-02-11T08:51:04.587660+07:00'
},
{
id: 2,
name: 'Xác nhận',
details: 'Đơn hàng được xác nhận',
icon: 'material-symbols:check-circle-outline-rounded',
color: 'green',
time: '2026-02-11T10:30:04.587660+07:00'
},
{
id: 3,
name: 'Xuất kho',
details: 'Hàng đã xuất kho',
icon: 'material-symbols:check-circle-outline-rounded',
color: 'green',
time: '2026-02-11T11:02:04.587660+07:00'
},
{
id: 4,
name: 'Đang giao',
details: 'Tài xế đang giao hàng',
icon: 'material-symbols:info-outline-rounded',
color: 'blue',
time: '2026-02-11T14:20:04.587660+07:00'
},
{
id: 5,
name: 'Giao hàng',
details: 'Đã giao thành công',
icon: 'material-symbols:check-circle-outline-rounded',
color: 'green',
time: '2026-02-11T17:38:04.587660+07:00'
},
]
</script>
<template>
<div>
<div class="is-flex is-flex-direction-column is-gap-2">
<div
v-for="item in historyItems"
:key="item.id"
class="is-flex is-gap-2"
>
<div class="is-flex is-flex-direction-column is-align-items-center is-gap-0.5">
<Icon
:name="item.icon"
:size="22"
:class="`has-text-${item.color}-40`"
/>
<div class="is-flex-grow-1 has-background-grey-lighter rounded-full"
style="width: 3px"
></div>
</div>
<div class="is-flex-grow-1">
<div class="is-flex is-gap-1 is-justify-content-space-between">
<div>
<p class="fs-15">{{ item.name }}</p>
<p class="fs-13 has-text-grey">{{ item.details }}</p>
</div>
<p class="is-family-monospace fs-12 has-text-grey">{{ $dayjs(item.time).format('HH:mm') }}</p>
</div>
<hr class="mt-4 mb-0 has-background-grey-95" style="height: 2px" />
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,66 @@
<script setup>
const props = defineProps({
order: Object
});
const { $dayjs, $shortenCurrency } = useNuxtApp();
const emit = defineEmits(['selectOrder', 'unselect']);
</script>
<template>
<div
:class="[
'card fs-14 is-clickable',
`has-background-${order.status__color}-95`
]"
:style="{ border: `1px solid var(--bulma-${order.status__color}-60)` }"
@click="selected ? emit('unselect') : emit('selectOrder', order.id)"
>
<div class="card-content p-4">
<div class="mb-4 is-flex is-justify-content-space-between is-gap-1">
<p class="fs-15 font-bold">{{ order.code }}</p>
<span
:class="['fs-13', `has-text-${order.payment_status__color}-40`]"
>
{{ order.payment_status__name }}
</span>
</div>
<div class="is-flex is-flex-direction-column is-gap-2">
<!-- customer info -->
<div>
<p class="has-text-grey-10">{{ order.customer__name }}</p>
<div class="has-text-grey fs-13 mt-1 is-flex is-gap-1 is-align-items-center">
<Icon name="material-symbols:call-outline-rounded" />
<p>{{ order.customer__phone }}</p>
</div>
</div>
<!-- product info -->
<div>
<p class="fs-24 has-text-grey-10 font-bold">{{ $shortenCurrency(order.total) }}</p>
<p class="fs-13 has-text-grey">{{ order.order__products.length }} sản phẩm</p>
</div>
<hr class="m-0" />
<div class="is-flex is-flex-direction-column is-gap-0.5 fs-13 has-text-grey">
<p class=" is-flex is-align-items-center is-gap-0.5">
<Icon name="material-symbols:calendar-today-outline-rounded" :size="16" />
<span>{{ $dayjs(order.create_time).format('L') }}</span>
<span></span>
<span>{{ $dayjs(order.create_time).format('HH:mm') }}</span>
</p>
<p>
NV: <span>{{ order.employee__name }}</span>
</p>
</div>
<button
v-if="order.status__name !== 'Hoàn thành'"
:class="[
'button fs-14 has-text-white',
order.status__name === 'Nháp' ? 'is-primary' : order.status__name === 'Đã xác nhận' ? 'is-orange' : 'is-success'
]"
>
{{ order.status__name === 'Nháp' ? 'Xác nhận' : order.status__name === 'Đã xác nhận' ? 'Giao hàng' : 'Hoàn thành' }}
</button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
const props = defineProps({
order: Object
});
const { $numtoString } = useNuxtApp();
</script>
<template>
<div>
<div class="is-flex is-gap-2">
<div class="is-flex-grow-1 p-3 rounded-md has-background-grey-95">
<p class="fs-13 has-text-grey">Tổng tiền</p>
<p class="font-bold">{{ $numtoString(order.total, { hasUnit: true }) }}</p>
</div>
<div class="is-flex-grow-1 p-3 rounded-md has-background-success-95">
<p class="fs-13 has-text-success-40">Đã thanh toán</p>
<p class="font-bold has-text-success-30">0 đ</p>
</div>
</div>
<div class="p-4 mt-4 rounded-md has-background-danger-95">
<p class="fs-13 has-text-danger-50">Còn lại</p>
<p class="fs-20 font-bold has-text-danger-40">{{ $numtoString(order.total, { hasUnit: true }) }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
const props = defineProps({
order: Object
});
const { $numtoString } = useNuxtApp();
</script>
<template>
<div>
<div class="is-flex is-flex-direction-column is-gap-2">
<div
v-for="product in order.order__products"
:key="product.id"
class="p-3 has-background-grey-95 rounded-md"
>
<div class="fs-15 is-flex is-justify-content-space-between">
<p class="">{{ product.name }}</p>
<p class="font-bold">{{ $numtoString(product.total, { hasUnit: true }) }}</p>
</div>
<div class="is-flex is-gap-8 fs-13 has-text-grey-50 mt-1">
<p>SL: {{ product.quantity }}</p>
<p>Đơn giá: {{ $numtoString(product.unit_price, { hasUnit: true }) }}</p>
<p>Giảm: {{ new Intl.NumberFormat("vi-VN", { style: "percent" }).format(product.discount) }}</p>
</div>
</div>
</div>
<hr />
<div class="font-bold is-flex is-gap-2 is-justify-content-space-between is-align-items-center">
<p class="fs-15">Tổng cộng</p>
<p class="fs-18">{{ $numtoString(order.total, { hasUnit: true }) }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
const props = defineProps({
order: Object
});
</script>
<template>
<div class="is-flex is-flex-direction-column is-gap-2 is-align-items-center">
<Icon
name="material-symbols:receipt-long-outline-rounded"
:size="50"
class="has-text-grey-70"
/>
<p>Chưa hoá đơn</p>
<button class="button is-primary has-background-purple">Tạo hoá đơn</button>
</div>
</template>

View File

@@ -59,7 +59,15 @@ const emit = defineEmits(['selectOrder', 'unselect'])
</div>
</td>
<td>
<button class="button is-dark fs-13 rounded-full">Xác nhận</button>
<button
v-if="order.status__name !== 'Hoàn thành'"
:class="[
'button fs-12 has-text-white rounded-lg',
order.status__name === 'Nháp' ? 'is-primary' : order.status__name === 'Đã xác nhận' ? 'is-orange' : 'is-success'
]"
>
{{ order.status__name === 'Nháp' ? 'Xác nhận' : order.status__name === 'Đã xác nhận' ? 'Giao hàng' : 'Hoàn thành' }}
</button>
</td>
</tr>
</template>

View File

@@ -35,10 +35,40 @@ const highlights = [
color: 'purple',
unit: 'VNĐ'
},
]
];
const viewModes = [
{ name: 'list', icon: 'material-symbols:format-list-bulleted-rounded' },
{ name: 'grid', icon: 'material-symbols:grid-view-outline-rounded' }
];
const viewMode = ref('list');
</script>
<template>
<div>
<div class="content is-flex is-gap-2 is-justify-content-flex-end is-align-items-center">
<div class="tabs is-toggle m-0">
<ul class="is-flex-grow-0 ml-auto">
<li
v-for="mode in viewModes"
:key="mode.name"
:class="[mode.name === viewMode && 'is-active']"
@click="viewMode = mode.name"
>
<a class="px-3 py-1">
<span class="icon m-0">
<Icon :name="mode.icon" :size="18" />
</span>
</a>
</li>
</ul>
</div>
<button class="button fs-14 is-primary">
<span class="icon">
<Icon :size="18" name="material-symbols:add-2-rounded" />
</span>
<span>Tạo đơn hàng</span>
</button>
</div>
<div class="fixed-grid has-2-cols-mobile has-5-cols">
<div class="grid">
<OrderHighlightCard
@@ -49,6 +79,13 @@ const highlights = [
</div>
</div>
<OrderPipeline />
<OrdersTable />
<OrdersTable :viewMode="viewMode" />
</div>
</template>
<style scoped>
.tabs {
--bulma-tabs-toggle-link-active-background-color: var(--bulma-link-90);
--bulma-tabs-toggle-link-active-border-color: var(--bulma-link-90);
--bulma-tabs-toggle-link-active-color: var(--bulma-link-40);
}
</style>

View File

@@ -0,0 +1,48 @@
<script setup>
import OrderKanbanCard from '@/components/orders/OrderKanbanCard.vue';
const props = defineProps({
orders: Array,
statuses: Array,
});
const emit = defineEmits(['selectOrder', 'unselect']);
</script>
<template>
<div class="p-5 fixed-grid has-4-cols">
<div class="grid">
<div
v-for="status in statuses"
:key="status.id"
class="card"
style="border: none"
>
<div class="card-content p-0 is-clipped">
<div :class="['p-4 is-flex is-justify-content-space-between is-align-items-center', `has-background-${status.color}-90`]">
<p class="font-semibold has-text-grey-10">{{ status.name }}</p>
<p class="px-2 py-1 font-semibold rounded-lg has-background-white"
:style="{ border: `1px solid var(--bulma-${status.color}-60)`}"
>
{{ orders.filter(o => o.status === status.id).length }}
</p>
</div>
<hr class="m-0 has-background-grey-80" />
<div class="has-background-grey-95 p-4">
<OrderKanbanCard
v-if="orders.filter(o => o.status === status.id).length > 0"
v-for="order in orders.filter(o => o.status === status.id)"
:key="order.id"
:order="order"
@selectOrder="emit('selectOrder', order.id)"
@unselect="emit('unselect')"
/>
<p v-else class="fs-13 has-text-centered has-text-grey">
Không đơn hàng
</p>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -3,6 +3,10 @@ import OrderRow from '@/components/orders/OrderRow.vue';
import SelectedOrder from '@/components/orders/SelectedOrder.vue';
import { pull } from 'es-toolkit';
const props = defineProps({
viewMode: String
});
const { $dayjs } = useNuxtApp();
const orders = [
@@ -480,7 +484,7 @@ function toggleStatus(id) {
</script>
<template>
<div>
<div class="card">
<div class="card is-clipped">
<div class="card-content">
<div class="is-flex is-gap-2 is-align-items-center">
<div class="field is-flex-grow-1 m-0">
@@ -556,8 +560,9 @@ function toggleStatus(id) {
<div class="fixed-grid has-3-cols">
<div class="grid">
<div :class="['cell', selectedOrder ? 'is-col-span-2' : 'is-col-span-3']">
<div class="card">
<div class="card is-clipped">
<div class="card-content p-0">
<template v-if="viewMode === 'list'">
<p class="p-5 fs-17 font-semibold is-flex is-align-items-center is-gap-1">
<Icon name="material-symbols:list-alt-outline-rounded" :size="22" />
<span>Danh sách đơn hàng ({{ filteredOrders.length }})</span>
@@ -587,6 +592,16 @@ function toggleStatus(id) {
/>
</tbody>
</table>
</template>
<OrdersKanban
v-else
:orders="filteredOrders"
:statuses="statuses"
@selectOrder="(id) => {
selectedOrder = filteredOrders.find(order => order.id === id);
}"
@unselect="selectedOrder = null"
/>
</div>
</div>
</div>

View File

@@ -1,10 +1,46 @@
<script setup>
import OrderDeliveryTab from '@/components/orders/OrderDeliveryTab.vue';
import OrderHistoryTab from '@/components/orders/OrderHistoryTab.vue';
import OrderPaymentTab from '@/components/orders/OrderPaymentTab.vue';
import OrderProductTab from '@/components/orders/OrderProductTab.vue';
import OrderReceiptTab from '@/components/orders/OrderReceiptTab.vue';
const props = defineProps({
order: Object
});
const { $dayjs, $numtoString } = useNuxtApp();
const emit = defineEmits(['unselect']);
const tabs = [
{
name: 'Chi tiết đơn',
heading: 'Chi tiết sản phẩm',
icon: 'material-symbols:deployed-code-outline'
},
{
name: 'Giao hàng',
heading: 'Thông tin giao hàng',
icon: 'material-symbols:delivery-truck-speed-outline-rounded'
},
{
name: 'Hoá đơn',
heading: 'Hoá đơn',
icon: 'material-symbols:receipt-long-outline-rounded'
},
{
name: 'Thanh toán',
heading: 'Thanh toán',
icon: 'material-symbols:credit-card-outline'
},
{
name: 'Lịch sử',
heading: 'Lịch sử đơn hàng',
icon: 'material-symbols:history-rounded'
},
];
const activeTab = ref(tabs[0]);
</script>
<template>
@@ -55,35 +91,32 @@ const emit = defineEmits(['unselect']);
<button class="button fs-14 is-flex-grow-1">Ghi thanh toán</button>
</div>
<hr class="m-0" />
<div class="tabs is-toggle fs-13">
<div class="tabs is-toggle m-0 fs-13">
<ul>
<li class="is-active">
<a>
<span>Chi tiết đơn</span>
</a>
</li>
<li>
<a>
<span>Giao hàng</span>
</a>
</li>
<li>
<a>
<span>Hoá đơn</span>
</a>
</li>
<li>
<a>
<span>Thanh toán</span>
</a>
</li>
<li>
<a>
<span>Lịch sử</span>
</a>
<li
v-for="tab in tabs"
:key="tab.name"
:class="activeTab.name === tab.name && 'is-active'"
@click="activeTab = tab"
>
<a>{{ tab.name }}</a>
</li>
</ul>
</div>
<hr class="m-0" />
<div id="tab-content">
<div class="is-flex is-gap-1 mb-4">
<Icon :name="activeTab.icon" :size="21" />
<span class="fs-15 font-semibold">{{ activeTab.heading }}</span>
</div>
<div>
<OrderProductTab v-if="activeTab.name === 'Chi tiết đơn'" :order="order" />
<OrderDeliveryTab v-else-if="activeTab.name === 'Giao hàng'" :order="order" />
<OrderReceiptTab v-else-if="activeTab.name === 'Hoá đơn'" :order="order" />
<OrderPaymentTab v-else-if="activeTab.name === 'Thanh toán'" :order="order" />
<OrderHistoryTab v-else-if="activeTab.name === 'Lịch sử'" :order="order" />
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,8 +1,6 @@
<template>
<ClientOnly>
<TopMenu @changetab="changeTab" />
</ClientOnly>
<ClientOnly>
<div class="container blockdiv has-text-text-20">
<div class="fs-17 font-semibold mb-2 is-flex is-align-items-center is-gap-1" v-if="tab">
<template v-if="subtab">
@@ -10,11 +8,16 @@
<SvgIcon class="mx-2" v-bind="{ name: 'right.svg', size: 17, type: 'has-text-black' }"></SvgIcon>
<span>{{ subtab[$store.lang] }}</span>
</template>
<span v-else>{{ tab[$store.lang] }}</span>
<div v-else>
<div class="is-flex is-gap-1 is-align-items-center mb-1">
<p>{{ tab[$store.lang] }}</p>
<button class="button is-primary is-light rounded-full p-1" @click="refresh()">
<Icon name="material-symbols:refresh-rounded" :size="24" />
<Icon name="material-symbols:refresh-rounded" :size="20" />
</button>
</div>
<p class="has-text-grey fs-13 font-normal">Cập nhật: 16:25:54</p>
</div>
</div>
<KeepAlive>
<component :is="componentMap[vbind.component]" v-bind="vbind" :key="componentKey" v-if="componentKey" />
</KeepAlive>

View File

@@ -11635,7 +11635,7 @@ a.navbar-item.is-active, a.navbar-item.is-selected,
margin: 0.5rem 0;
}
@media screen and (max-width: 1023px) {
@media screen and (max-width: 767px) {
.navbar > .container {
display: block;
}
@@ -11684,7 +11684,7 @@ a.navbar-item.is-active, a.navbar-item.is-selected,
padding-bottom: var(--bulma-navbar-height);
}
}
@media screen and (min-width: 1024px) {
@media screen and (min-width: 768px) {
.navbar,
.navbar-menu,
.navbar-start,

File diff suppressed because one or more lines are too long

View File

@@ -13,6 +13,7 @@ $grey: #767676;
@use "bulma/sass" with (
$family-primary: string.unquote("'Inter', 'SF Pro', 'Helvetica', 'Arial', sans-serif"),
$family-monospace: string.unquote("'Roboto Mono', monospace"),
$navbar-breakpoint: 768px,
$primary: $blue,
$link: $blue,
$info: $cyan,