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

@@ -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

@@ -10,7 +10,7 @@ const emit = defineEmits(['selectOrder', 'unselect'])
<template>
<tr
:class="['is-clickable', selected && 'is-selected']"
@click="selected ? emit('unselect') : emit('selectOrder', order.id) "
@click="selected ? emit('unselect') : emit('selectOrder', order.id)"
>
<td>
<div>
@@ -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,37 +560,48 @@ 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">
<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>
</p>
<table class="table is-fullwidth is-hoverable fs-13">
<thead>
<tr>
<th class="font-semibold">Đơn hàng</th>
<th class="font-semibold">Khách hàng</th>
<th class="font-semibold has-text-right">Tổng tiền</th>
<th class="font-semibold has-text-centered">Trạng thái</th>
<th class="font-semibold">Thanh toán</th>
<th class="font-semibold">Giao hàng</th>
<th class="font-semibold">Ngày tạo</th>
<th class="font-semibold">Thao tác</th>
</tr>
</thead>
<tbody>
<OrderRow
v-for="order in filteredOrders"
:key="order.id"
v-bind="{ order, selected: order.id === selectedOrder?.id }"
@selectOrder="(id) => {
selectedOrder = filteredOrders.find(order => order.id === id);
}"
@unselect="selectedOrder = null"
/>
</tbody>
</table>
<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>
</p>
<table class="table is-fullwidth is-hoverable fs-13">
<thead>
<tr>
<th class="font-semibold">Đơn hàng</th>
<th class="font-semibold">Khách hàng</th>
<th class="font-semibold has-text-right">Tng tiền</th>
<th class="font-semibold has-text-centered">Trạng thái</th>
<th class="font-semibold">Thanh toán</th>
<th class="font-semibold">Giao hàng</th>
<th class="font-semibold">Ngày tạo</th>
<th class="font-semibold">Thao tác</th>
</tr>
</thead>
<tbody>
<OrderRow
v-for="order in filteredOrders"
:key="order.id"
v-bind="{ order, selected: order.id === selectedOrder?.id }"
@selectOrder="(id) => {
selectedOrder = filteredOrders.find(order => order.id === id);
}"
@unselect="selectedOrder = null"
/>
</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>