This commit is contained in:
Viet An
2026-06-18 08:41:33 +07:00
parent a8aa9a3dce
commit b7c82e944d
17 changed files with 531 additions and 775 deletions

View File

@@ -0,0 +1,118 @@
<script setup>
const props = defineProps({
invoice: Object,
});
const { $dayjs, $shortenCurrency } = useNuxtApp();
const showModal = ref();
function openModal() {
showModal.value = {
component: "orders/SelectedOrder",
title: "Chi tiết đơn hàng",
width: "75%",
height: "500px",
vbind: {
invoice: props.invoice,
},
};
}
</script>
<template>
<tr
class="is-clickable"
@click="openModal"
>
<td>
<div>
<p class="fs-15 font-semibold">{{ invoice.code }}</p>
<p class="has-text-grey">{{ invoice.staff__fullname }}</p>
</div>
</td>
<td>
<div>
<p>{{ invoice.customer_name }}</p>
<div class="is-flex is-gap-0.5 is-align-items-center mt-1 has-text-grey">
<Icon
name="material-symbols:call-outline-rounded"
:size="15"
/>
<span class="fs-12">{{ invoice.customer_phone }}</span>
</div>
</div>
</td>
<td class="has-text-right">
<div>
<p class="fs-14 font-semibold">{{ $shortenCurrency(invoice.total_amount) }}</p>
<p class="has-text-grey">{{ invoice.product_amount }} SP</p>
</div>
</td>
<td class="has-text-centered">
<span
:class="['tag rounded-full', `has-background-${invoice.status__color}-80 has-text-${invoice.status__color}-25`]"
>
{{ invoice.status }}
</span>
</td>
<td>
<p :class="`has-text-${invoice.payment_status__color}-40`">
{{ invoice.payment_status__name }}
</p>
</td>
<td>
<p :class="`has-text-${invoice.delivery_status__color}-40`">
{{ invoice.delivery__status }}
</p>
</td>
<td>
<div>
<div class="is-flex is-gap-0.5">
<Icon
:size="18"
name="material-symbols:calendar-today-outline-rounded"
/>
<span>{{ $dayjs(invoice.create_time).format("L") }}</span>
</div>
<p class="has-text-grey fs-12">
{{ $dayjs(invoice.create_time).format("HH:mm") }}
</p>
</div>
</td>
<td>
<button
v-if="invoice.status__name !== 'Hoàn thành'"
:class="[
'button fs-12 has-text-white rounded-lg',
invoice.status__name === 'Nháp'
? 'is-primary'
: invoice.status__name === 'Đã xác nhận'
? 'is-orange'
: 'is-success',
]"
>
{{
invoice.status__name === "Nháp"
? "Xác nhận"
: invoice.status__name === "Đã xác nhận"
? "Giao hàng"
: "Hoàn thành"
}}
</button>
</td>
<Modal
v-if="showModal"
v-bind="showModal"
@close="showModal = null"
/>
</tr>
</template>
<style scoped>
tr.is-selected {
--bulma-table-row-active-background-color: var(--bulma-primary-95);
color: unset;
}
td {
vertical-align: middle;
--bulma-table-cell-padding: 0.75em;
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
order: Object, invoice: Object,
}); });
</script> </script>
@@ -9,13 +9,13 @@ const props = defineProps({
<div class="p-4 rounded-md has-background-primary-95"> <div class="p-4 rounded-md has-background-primary-95">
<p class="has-text-grey">Trạng thái</p> <p class="has-text-grey">Trạng thái</p>
<p class="fs-17 mt-1 font-semibold has-text-primary-50"> <p class="fs-17 mt-1 font-semibold has-text-primary-50">
{{ order.delivery_status__name }} {{ invoice.delivery_status__name }}
</p> </p>
</div> </div>
<div class="p-4 rounded-md has-background-white-bis"> <div class="p-4 rounded-md has-background-white-bis">
<p class="has-text-grey">Địa chỉ giao hàng</p> <p class="has-text-grey">Địa chỉ giao hàng</p>
<p class="mt-1 has-text-grey-darker">{{ order.customer__name }}</p> <p class="mt-1 has-text-grey-darker">{{ invoice.customer__name }}</p>
<p class="has-text-grey-darker">{{ order.customer__phone }}</p> <p class="has-text-grey-darker">{{ invoice.customer__phone }}</p>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
order: Object, invoice: Object,
}); });
const { $dayjs } = useNuxtApp(); const { $dayjs } = useNuxtApp();

View File

@@ -5,6 +5,19 @@ const props = defineProps({
const { $dayjs, $shortenCurrency } = useNuxtApp(); const { $dayjs, $shortenCurrency } = useNuxtApp();
const emit = defineEmits(["selectOrder", "unselect"]); const emit = defineEmits(["selectOrder", "unselect"]);
const showModal = ref();
function openModal() {
showModal.value = {
component: "orders/SelectedOrder",
title: "SelectedOrder",
width: "75%",
height: "500px",
vbind: {
order: props.order,
},
};
}
</script> </script>
<template> <template>
@@ -72,5 +85,10 @@ const emit = defineEmits(["selectOrder", "unselect"]);
</button> </button>
</div> </div>
</div> </div>
<Modal
v-if="showModal"
v-bind="showModal"
@close="showModal = null"
/>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
order: Object, invoice: Object,
}); });
const { $numtoString } = useNuxtApp(); const { $numtoString } = useNuxtApp();
</script> </script>
@@ -11,7 +11,7 @@ const { $numtoString } = useNuxtApp();
<div class="is-flex-grow-1 p-3 rounded-md has-background-white-bis"> <div class="is-flex-grow-1 p-3 rounded-md has-background-white-bis">
<p class="fs-13 has-text-grey">Tổng tiền</p> <p class="fs-13 has-text-grey">Tổng tiền</p>
<p class="font-bold"> <p class="font-bold">
{{ $numtoString(order.total, { hasUnit: true }) }} {{ $numtoString(invoice.total, { hasUnit: true }) }}
</p> </p>
</div> </div>
<div class="is-flex-grow-1 p-3 rounded-md has-background-success-95"> <div class="is-flex-grow-1 p-3 rounded-md has-background-success-95">
@@ -22,7 +22,7 @@ const { $numtoString } = useNuxtApp();
<div class="p-4 mt-4 rounded-md has-background-danger-95"> <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-13 has-text-danger-50">Còn lại</p>
<p class="fs-20 font-bold has-text-danger-40"> <p class="fs-20 font-bold has-text-danger-40">
{{ $numtoString(order.total, { hasUnit: true }) }} {{ $numtoString(invoice.total, { hasUnit: true }) }}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,39 +1,66 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
order: Object, invoice: Object,
}); });
const { $numtoString } = useNuxtApp(); const { $getdata, $numtoString } = useNuxtApp();
const invoiceDetails = ref([]);
onMounted(async () => {
invoiceDetails.value = await $getdata("Invoice_Detail", { filter: { invoice: props.invoice.id } });
});
</script> </script>
<template> <template>
<div> <div>
<pre class="fs-12 max-h-40 mb-2">{{ invoiceDetails }}</pre>
<div class="is-flex is-flex-direction-column is-gap-2"> <div class="is-flex is-flex-direction-column is-gap-2">
<div <div
v-for="product in order.order__products" v-for="invoiceDetail in invoiceDetails"
:key="product.id" :key="invoiceDetail.id"
class="p-3 has-background-white-ter rounded-md" class="p-3 has-background-white-ter rounded-md"
> >
<div class="fs-15 is-flex is-justify-content-space-between"> <div class="fs-15 is-flex is-justify-content-space-between">
<p class="">{{ product.name }}</p> <p>{{ invoiceDetail.variant__product__name }}</p>
<p class="font-bold"> <p class="font-semibold">
{{ $numtoString(product.total, { hasUnit: true }) }} {{ $numtoString(invoiceDetail.price, { hasUnit: true }) }}
</p> </p>
</div> </div>
<div class="is-flex is-gap-8 fs-13 has-text-grey-dark mt-1"> <div class="is-flex is-gap-8 fs-13 has-text-grey-dark mt-1">
<p>SL: {{ product.quantity }}</p> <p>SL: {{ invoiceDetail.quantity }}</p>
<p>Đơn giá: {{ $numtoString(product.unit_price, { hasUnit: true }) }}</p> <p>Đơn giá: {{ $numtoString(invoiceDetail.price, { hasUnit: true }) }}</p>
<p> <p>
Giảm: Giảm:
{{ new Intl.NumberFormat("vi-VN", { style: "percent" }).format(product.discount) }} {{ new Intl.NumberFormat("vi-VN", { style: "percent" }).format(invoiceDetail.discount) }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
<div class="font-bold is-flex is-gap-2 is-justify-content-space-between is-align-items-center"> <table class="table is-fullwidth is-bordered fs-14 mb-8">
<p class="fs-15">Tổng cộng</p> <tbody class="has-text-right">
<p class="fs-18">{{ $numtoString(order.total, { hasUnit: true }) }}</p> <tr>
</div> <td class="has-text-grey-dark">Tổng tiền hàng</td>
<td>{{ $numtoString(invoice.total_amount, { hasUnit: true }) }}</td>
</tr>
<tr>
<td class="has-text-grey-dark">Phí vận chuyển</td>
<td>{{ $numtoString(invoice.shipping_fee, { hasUnit: true }) }}</td>
</tr>
<tr>
<td class="has-text-grey-dark">Giảm giá</td>
<td>{{ $numtoString(invoice.discount_amount, { hasUnit: true }) }}</td>
</tr>
<tr class="font-bold">
<td>Tổng cộng</td>
<td class="fs-17">{{ $numtoString(invoice.final_amount, { hasUnit: true }) }}</td>
</tr>
</tbody>
</table>
</div> </div>
</template> </template>
<style scoped>
table td {
vertical-align: middle;
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
order: Object, invoice: Object,
}); });
</script> </script>

View File

@@ -1,98 +0,0 @@
<script setup>
const props = defineProps({
order: Object,
selected: Boolean,
});
const { $dayjs, $shortenCurrency } = useNuxtApp();
const emit = defineEmits(["selectOrder", "unselect"]);
</script>
<template>
<tr
:class="['is-clickable', selected && 'is-selected']"
@click="selected ? emit('unselect') : emit('selectOrder', order.id)"
>
<td>
<div>
<p class="fs-15 font-semibold">{{ order.code }}</p>
<p class="has-text-grey">{{ order.employee__name }}</p>
</div>
</td>
<td>
<div>
<p>{{ order.customer__name }}</p>
<div class="is-flex is-gap-0.5 is-align-items-center mt-1 has-text-grey">
<Icon
name="material-symbols:call-outline-rounded"
:size="15"
/>
<span class="fs-12">{{ order.customer__phone }}</span>
</div>
</div>
</td>
<td class="has-text-right">
<div>
<p class="fs-14 font-semibold">{{ $shortenCurrency(order.total) }}</p>
<p class="has-text-grey">{{ order.order__products.length }} SP</p>
</div>
</td>
<td class="has-text-centered">
<span
:class="['tag rounded-full', `has-background-${order.status__color}-80 has-text-${order.status__color}-25`]"
>
{{ order.status__name }}
</span>
</td>
<td>
<p :class="`has-text-${order.payment_status__color}-40`">
{{ order.payment_status__name }}
</p>
</td>
<td>
<p :class="`has-text-${order.delivery_status__color}-40`">
{{ order.delivery_status__name }}
</p>
</td>
<td>
<div>
<div class="is-flex is-gap-0.5">
<Icon
:size="18"
name="material-symbols:calendar-today-outline-rounded"
/>
<span>{{ $dayjs(order.create_time).format("L") }}</span>
</div>
<p class="has-text-grey fs-12">
{{ $dayjs(order.create_time).format("HH:mm") }}
</p>
</div>
</td>
<td>
<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>
<style scoped>
tr.is-selected {
--bulma-table-row-active-background-color: var(--bulma-primary-95);
color: unset;
}
td {
vertical-align: middle;
--bulma-table-cell-padding: 0.75em;
}
</style>

View File

@@ -3,7 +3,7 @@ import OrderHighlightCard from "~/components/orders/OrderHighlightCard.vue";
import OrderPipeline from "~/components/orders/OrderPipeline.vue"; import OrderPipeline from "~/components/orders/OrderPipeline.vue";
import OrdersTable from "~/components/orders/OrdersTable.vue"; import OrdersTable from "~/components/orders/OrdersTable.vue";
const { $store } = useNuxtApp(); const { $getdata, $store } = useNuxtApp();
const highlights = [ const highlights = [
{ {
name: "Nháp", name: "Nháp",
@@ -43,6 +43,14 @@ const viewModes = [
{ name: "grid", icon: "material-symbols:grid-view-outline-rounded" }, { name: "grid", icon: "material-symbols:grid-view-outline-rounded" },
]; ];
const viewMode = ref("list"); const viewMode = ref("list");
const invoices = ref();
onMounted(async () => {
invoices.value = await $getdata("Invoice");
});
provide("orders", {
invoices,
});
</script> </script>
<template> <template>
<div> <div>
@@ -82,7 +90,7 @@ const viewMode = ref("list");
</button> </button>
</div> </div>
</Teleport> </Teleport>
<div class="fixed-grid has-2-cols-mobile has-5-cols"> <!-- <div class="fixed-grid has-2-cols-mobile has-5-cols">
<div class="grid"> <div class="grid">
<OrderHighlightCard <OrderHighlightCard
v-for="highlight in highlights" v-for="highlight in highlights"
@@ -91,7 +99,7 @@ const viewMode = ref("list");
/> />
</div> </div>
</div> </div>
<OrderPipeline /> <OrderPipeline /> -->
<OrdersTable :viewMode="viewMode" /> <OrdersTable :viewMode="viewMode" />
</div> </div>
</template> </template>

View File

@@ -1,376 +1,14 @@
<script setup> <script setup>
import OrderRow from "~/components/orders/OrderRow.vue";
import SelectedOrder from "~/components/orders/SelectedOrder.vue";
import { pull } from "es-toolkit"; import { pull } from "es-toolkit";
import InvoiceRow from "~/components/orders/InvoiceRow.vue";
import OrdersKanban from "~/components/orders/OrdersKanban.vue";
const props = defineProps({ const props = defineProps({
viewMode: String, viewMode: String,
}); });
const { $dayjs } = useNuxtApp(); const { $dayjs } = useNuxtApp();
const { invoices } = inject("orders");
const orders = [
{
id: 1,
code: "SO001",
employee: 1,
employee__name: "Trần Thị B",
customer: 1,
customer__name: "Nguyễn Văn A",
customer__phone: "0901234567",
total: "5200000.00",
status: 1,
status__name: "Nháp",
status__color: "yellow",
payment_status: 1,
payment_status__name: "Chưa thanh toán",
payment_status__color: "red",
delivery_status: 1,
delivery_status__name: "Chờ xử lý",
delivery_status__color: "grey",
order__products: [
{
id: 1,
name: "Kem chống nắng SPF 50+",
unit_price: "280000.00",
quantity: 10,
discount: null,
total: "2800000.00",
},
{
id: 2,
name: "Serum Vitamin C 30ml",
unit_price: "450000.00",
quantity: 5,
discount: 0.1,
total: "2025000.00",
},
{
id: 3,
name: "Son dưỡng môi vitamin E",
unit_price: "75000.00",
quantity: 5,
discount: null,
total: "375000.00",
},
],
create_time: "2026-02-11T08:51:04.587660+07:00",
},
{
id: 2,
code: "SO002",
employee: 1,
employee__name: "Trần Thị B",
customer: 3,
customer__name: "Lê Thị C",
customer__phone: "0912345678",
total: "8500000.00",
status: 2,
status__name: "Đã xác nhận",
status__color: "blue",
payment_status: 2,
payment_status__name: "Một phần",
payment_status__color: "yellow",
delivery_status: 2,
delivery_status__name: "Sẵn sàng",
delivery_status__color: "blue",
order__products: [
{
id: 3,
name: "Mặt nạ collagen 10 miếng",
unit_price: "320000.00",
quantity: 15,
discount: 0.05,
total: "4560000.00",
},
{
id: 4,
name: "Toner cân bằng da 200ml",
unit_price: "180000.00",
quantity: 20,
discount: 0.1,
total: "3240000.00",
},
{
id: 5,
name: "Gel rửa mặt làm sạch sâu",
unit_price: "140000.00",
quantity: 5,
discount: null,
total: "700000.00",
},
],
create_time: "2026-04-06T09:10:09.587660+07:00",
},
{
id: 3,
code: "SO003",
employee: 5,
employee__name: "Hoàng Văn E",
customer: 4,
customer__name: "Phạm Văn D",
customer__phone: "0923456703",
total: "12300000.00",
status: 3,
status__name: "Đang giao",
status__color: "orange",
payment_status: 3,
payment_status__name: "Đã thanh toán",
payment_status__color: "green",
delivery_status: 3,
delivery_status__name: "Hoàn thành",
delivery_status__color: "green",
order__products: [
{
id: 6,
name: "Kem dưỡng ẩm ban đêm",
unit_price: "380000.00",
quantity: 20,
discount: 0.05,
total: "7220000.00",
},
{
id: 7,
name: "Phấn nền BB cream",
unit_price: "220000.00",
quantity: 15,
discount: null,
total: "3300000.00",
},
{
id: 8,
name: "Nước tẩy trang 3 trong 1",
unit_price: "195000.00",
quantity: 10,
discount: 0.1,
total: "1755000.00",
},
],
create_time: "2026-04-05T02:33:24.587660+07:00",
},
{
id: 4,
code: "SO004",
employee: 1,
employee__name: "Trần Thị B",
customer: 6,
customer__name: "Vũ Thị F",
customer__phone: "0934835222",
total: "6800000.00",
status: 4,
status__name: "Hoàn thành",
status__color: "green",
payment_status: 3,
payment_status__name: "Đã thanh toán",
payment_status__color: "green",
delivery_status: 3,
delivery_status__name: "Hoàn thành",
delivery_status__color: "green",
order__products: [
{
id: 9,
name: "Dầu gội thảo mộc 500ml",
unit_price: "120000.00",
quantity: 30,
discount: 0.1,
total: "3240000.00",
},
{
id: 10,
name: "Sữa rửa mặt trà xanh 150ml",
unit_price: "150000.00",
quantity: 20,
discount: 0.05,
total: "2850000.00",
},
{
id: 11,
name: "Son dưỡng môi vitamin E",
unit_price: "75000.00",
quantity: 10,
discount: 0.05,
total: "712500.00",
},
],
create_time: "2026-04-04T23:21:11.587660+07:00",
},
{
id: 5,
code: "SO005",
employee: 5,
employee__name: "Hoàng Văn E",
customer: 7,
customer__name: "Đỗ Văn G",
customer__phone: "0945781113",
total: "9400000.00",
status: 2,
status__name: "Đã xác nhận",
status__color: "blue",
payment_status: 1,
payment_status__name: "Chưa thanh toán",
payment_status__color: "red",
delivery_status: 1,
delivery_status__name: "Chờ xử lý",
delivery_status__color: "grey",
order__products: [
{
id: 12,
name: "Kem mắt chống lão hóa",
unit_price: "550000.00",
quantity: 10,
discount: null,
total: "5500000.00",
},
{
id: 13,
name: "Serum Vitamin C 30ml",
unit_price: "450000.00",
quantity: 8,
discount: 0.05,
total: "3420000.00",
},
{
id: 14,
name: "Gel rửa mặt làm sạch sâu",
unit_price: "140000.00",
quantity: 5,
discount: 0.15,
total: "595000.00",
},
],
create_time: "2026-04-07T11:21:46.587660+07:00",
},
{
id: 6,
code: "SO006",
employee: 1,
employee__name: "Trần Thị B",
customer: 8,
customer__name: "Bùi Thị H",
customer__phone: "0933184392",
total: "4200000.00",
status: 1,
status__name: "Nháp",
status__color: "yellow",
payment_status: 1,
payment_status__name: "Chưa thanh toán",
payment_status__color: "red",
delivery_status: 1,
delivery_status__name: "Chờ xử lý",
delivery_status__color: "grey",
order__products: [
{
id: 15,
name: "Toner cân bằng da 200ml",
unit_price: "180000.00",
quantity: 15,
discount: null,
total: "2700000.00",
},
{
id: 16,
name: "Mặt nạ collagen 10 miếng",
unit_price: "320000.00",
quantity: 5,
discount: 0.05,
total: "1520000.00",
},
],
create_time: "2026-04-07T13:17:36.587660+07:00",
},
{
id: 7,
code: "SO007",
employee: 5,
employee__name: "Hoàng Văn E",
customer: 9,
customer__name: "Ngô Văn I",
customer__phone: "0978335172",
total: "15600000.00",
status: 3,
status__name: "Đang giao",
status__color: "orange",
payment_status: 3,
payment_status__name: "Đã thanh toán",
payment_status__color: "green",
delivery_status: 2,
delivery_status__name: "Hoàn thành",
delivery_status__color: "green",
order__products: [
{
id: 17,
name: "Kem chống nắng SPF 50+",
unit_price: "280000.00",
quantity: 30,
discount: 0.1,
total: "7560000.00",
},
{
id: 18,
name: "Phấn nền BB cream",
unit_price: "220000.00",
quantity: 25,
discount: 0.05,
total: "5225000.00",
},
{
id: 19,
name: "Kem dưỡng ẩm ban đêm",
unit_price: "380000.00",
quantity: 8,
discount: 0.05,
total: "2880000.00",
},
],
create_time: "2026-06-04T09:30:12.587660+07:00",
},
{
id: 8,
code: "SO008",
employee: 1,
employee__name: "Trần Thị B",
customer: 11,
customer__name: "Đinh Thị K",
customer__phone: "0922104853",
total: "7900000.00",
status: 2,
status__name: "Đã xác nhận",
status__color: "blue",
payment_status: 2,
payment_status__name: "Một phần",
payment_status__color: "yellow",
delivery_status: 2,
delivery_status__name: "Sẵn sàng",
delivery_status__color: "blue",
order__products: [
{
id: 20,
name: "Nước tẩy trang 3 trong 1",
unit_price: "195000.00",
quantity: 20,
discount: 0.05,
total: "3705000.00",
},
{
id: 21,
name: "Sữa rửa mặt trà xanh 150ml",
unit_price: "150000.00",
quantity: 18,
discount: null,
total: "2700000.00",
},
{
id: 22,
name: "Dầu gội thảo mộc 500ml",
unit_price: "120000.00",
quantity: 13,
discount: 0.05,
total: "1482000.00",
},
],
create_time: "2026-04-06T16:01:22.587660+07:00",
},
];
const statuses = [ const statuses = [
{ {
@@ -434,8 +72,9 @@ const selectedStatuses = ref([]);
const selectedPaymentStatus = ref(); const selectedPaymentStatus = ref();
const selectedEmployee = ref(); const selectedEmployee = ref();
const filteredOrders = computed(() => { const filteredInvoices = computed(() => {
const filteredByInput = orders.filter((order) => { if (!invoices.value) return [];
const filteredByInput = invoices.value.filter((order) => {
if (!input.value) return true; if (!input.value) return true;
const values = Object.values(order); const values = Object.values(order);
const strValues = values.filter((v) => typeof v === "string"); const strValues = values.filter((v) => typeof v === "string");
@@ -470,7 +109,7 @@ const filteredOrders = computed(() => {
const selectedOrder = ref(null); const selectedOrder = ref(null);
watch(filteredOrders, () => { watch(filteredInvoices, () => {
selectedOrder.value = null; selectedOrder.value = null;
}); });
@@ -484,7 +123,10 @@ function toggleStatus(id) {
</script> </script>
<template> <template>
<div> <div>
<div class="card"> <div
v-if="true"
class="card"
>
<div class="card-content"> <div class="card-content">
<div class="is-flex is-gap-2 is-align-items-center"> <div class="is-flex is-gap-2 is-align-items-center">
<div class="field is-flex-grow-1 m-0"> <div class="field is-flex-grow-1 m-0">
@@ -569,69 +211,45 @@ function toggleStatus(id) {
</div> </div>
</div> </div>
</div> </div>
<div class="fixed-grid has-3-cols"> <div class="card is-clipped">
<div class="grid"> <div class="card-content p-0">
<div :class="['cell', selectedOrder ? 'is-col-span-2' : 'is-col-span-3']"> <p class="p-5 icon-text fs-16 font-semibold w-full">
<div class="card is-clipped"> <span class="icon">
<div class="card-content p-0"> <Icon
<template v-if="viewMode === 'list'"> name="material-symbols:list-alt-outline-rounded"
<p class="p-5 icon-text fs-16 font-semibold w-full"> :size="22"
<span class="icon"> />
<Icon </span>
name="material-symbols:list-alt-outline-rounded" <span>Danh sách đơn hàng ({{ filteredInvoices.length }})</span>
:size="22" </p>
/> <table
</span> v-if="viewMode === 'list'"
<span>Danh sách đơn hàng ({{ filteredOrders.length }})</span> class="table is-fullwidth is-hoverable fs-13"
</p> >
<table class="table is-fullwidth is-hoverable fs-13"> <thead>
<thead> <tr>
<tr> <th class="font-semibold">Đơn hàng</th>
<th class="font-semibold">Đơn hàng</th> <th class="font-semibold">Khách 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-right">Tng tiền</th> <th class="font-semibold has-text-centered">Trạng thái</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">Thanh toán</th> <th class="font-semibold">Giao hàng</th>
<th class="font-semibold">Giao hàng</th> <th class="font-semibold">Ngày tạo</th>
<th class="font-semibold">Ngày tạo</th> <th class="font-semibold">Thao tác</th>
<th class="font-semibold">Thao tác</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <InvoiceRow
<OrderRow v-for="invoice in filteredInvoices"
v-for="order in filteredOrders" :key="invoice.id"
:key="order.id" :invoice="invoice"
v-bind="{ />
order, </tbody>
selected: order.id === selectedOrder?.id, </table>
}" <OrdersKanban
@selectOrder=" v-else
(id) => { :orders="filteredInvoices"
selectedOrder = filteredOrders.find((order) => order.id === id); :statuses="statuses"
}
"
@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>
<SelectedOrder
:order="selectedOrder"
@unselect="selectedOrder = null"
/> />
</div> </div>
</div> </div>

View File

@@ -6,7 +6,7 @@ import OrderProductTab from "~/components/orders/OrderProductTab.vue";
import OrderReceiptTab from "~/components/orders/OrderReceiptTab.vue"; import OrderReceiptTab from "~/components/orders/OrderReceiptTab.vue";
const props = defineProps({ const props = defineProps({
order: Object, invoice: Object,
}); });
const { $dayjs, $numtoString } = useNuxtApp(); const { $dayjs, $numtoString } = useNuxtApp();
@@ -44,131 +44,112 @@ const activeTab = ref(tabs[0]);
</script> </script>
<template> <template>
<div <div v-if="invoice">
v-if="order" <div>
class="cell relative fs-14" <div class="is-flex is-gap-1.5 is-align-items-center">
> <span class="fs-17 font-bold">{{ invoice.code }}</span>
<div class="card"> <span
<button :class="[
@click="emit('unselect')" 'tag rounded-full',
class="button is-white rounded-full has-text-grey absolute size-8 is-flex is-justify-content-center is-align-items-center" `has-background-${invoice.status__color}-80 has-text-${invoice.status__color}-25`,
style="right: 0.5rem; top: 0.5rem" ]"
> >
<span class="icon"> {{ invoice.status }}
<Icon
name="material-symbols:close-rounded"
:size="22"
/>
</span> </span>
</button> </div>
<div class="card-content"> <div class="is-flex is-gap-0.5 is-flex-direction-column mt-4">
<div> <p class="icon-text">
<div class="is-flex is-gap-2 is-align-items-center"> <span class="icon">
<span class="fs-17 font-bold">{{ order.code }}</span>
<span
:class="[
'tag rounded-full',
`has-background-${order.status__color}-80 has-text-${order.status__color}-25`,
]"
>
{{ order.status__name }}
</span>
</div>
<div class="is-flex is-gap-0.5 is-flex-direction-column mt-2">
<div class="is-flex is-gap-1 is-align-items-center">
<Icon
name="material-symbols:person-outline-rounded"
:size="18"
/>
<p>{{ order.customer__name }}</p>
</div>
<div class="is-flex is-gap-1 is-align-items-center">
<Icon
name="material-symbols:call-outline-rounded"
:size="18"
/>
<p>{{ order.customer__phone }}</p>
</div>
<div class="is-flex is-gap-1 is-align-items-center">
<Icon
name="material-symbols:calendar-today-outline-rounded"
:size="18"
/>
<p>{{ $dayjs(order.create_time).format("LL") }}</p>
</div>
</div>
<div class="p-4 has-background-primary-95 rounded-lg mt-6">
<p>Tổng giá trị đơn hàng</p>
<p class="font-bold fs-28">
{{ $numtoString(order.total, { hasUnit: true }) }}
</p>
</div>
</div>
<hr class="m-0" />
<div class="buttons m-0">
<button class="button fs-14 is-flex-grow-1 is-primary">Xác nhận đơn</button>
<button class="button fs-14 is-flex-grow-1">Ghi thanh toán</button>
</div>
<hr class="m-0" />
<div class="tabs is-toggle m-0 fs-13">
<ul>
<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 <Icon
:name="activeTab.icon" name="material-symbols:person-outline-rounded"
:size="21" :size="18"
/> />
<span class="fs-15 font-semibold">{{ activeTab.heading }}</span> </span>
</div> <span>{{ invoice.customer_name }}</span>
<div> </p>
<OrderProductTab <p class="icon-text">
v-if="activeTab.name === 'Chi tiết đơn'" <span class="icon">
:order="order" <Icon
name="material-symbols:call-outline-rounded"
:size="18"
/> />
<OrderDeliveryTab </span>
v-else-if="activeTab.name === 'Giao hàng'" <span>{{ invoice.customer_phone }}</span>
:order="order" </p>
<p class="icon-text">
<span class="icon">
<Icon
name="material-symbols:calendar-today-outline-rounded"
:size="18"
/> />
<OrderReceiptTab </span>
v-else-if="activeTab.name === 'Hoá đơn'" <span>{{ $dayjs(invoice.create_time).format("LL") }}</span>
:order="order" </p>
/> </div>
<OrderPaymentTab <div class="px-6 py-4 has-background-primary-95 rounded-lg mt-6">
v-else-if="activeTab.name === 'Thanh toán'" <p>Tổng giá trị đơn hàng</p>
:order="order" <p class="font-bold fs-25">
/> {{ $numtoString(invoice.total_amount, { hasUnit: true }) }}
<OrderHistoryTab </p>
v-else-if="activeTab.name === 'Lịch sử'" </div>
:order="order" </div>
/> <hr />
</div> <div class="buttons m-0">
</div> <button class="button fs-14 is-flex-grow-1 is-primary">Xác nhận đơn</button>
<button class="button fs-14 is-flex-grow-1">Ghi thanh toán</button>
</div>
<hr />
<div class="tabs is-toggle m-0 fs-13">
<ul>
<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>
<div
id="tab-content"
class="mt-4"
>
<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'"
:invoice="invoice"
/>
<OrderDeliveryTab
v-else-if="activeTab.name === 'Giao hàng'"
:invoice="invoice"
/>
<OrderReceiptTab
v-else-if="activeTab.name === 'Hoá đơn'"
:invoice="invoice"
/>
<OrderPaymentTab
v-else-if="activeTab.name === 'Thanh toán'"
:invoice="invoice"
/>
<OrderHistoryTab
v-else-if="activeTab.name === 'Lịch sử'"
:invoice="invoice"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.card-content {
padding: 0; /* 1.5rem */
> div {
padding: var(--bulma-card-content-padding);
}
}
.tabs { .tabs {
--bulma-tabs-toggle-link-active-background-color: var(--bulma-link-90); --bulma-tabs-toggle-link-active-background-color: var(--bulma-link-95);
--bulma-tabs-toggle-link-active-border-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); --bulma-tabs-toggle-link-active-color: var(--bulma-link-40);
} }

View File

@@ -17,15 +17,17 @@ function openModal() {
</script> </script>
<template> <template>
<button <div>
@click="openModal" <button
class="button is-ghost fs-13" @click="openModal"
> class="button is-ghost fs-13"
Chọn IMEI >
</button> Chọn IMEI
<Modal </button>
v-if="showmodal" <Modal
v-bind="showmodal" v-if="showmodal"
@close="showmodal = null" v-bind="showmodal"
/> @close="showmodal = null"
/>
</div>
</template> </template>

View File

@@ -70,86 +70,88 @@ onMounted(fetchImeis);
</script> </script>
<template> <template>
<div <div>
v-if="isLoading" <div
class="is-flex is-justify-content-center" v-if="isLoading"
> class="is-flex is-justify-content-center"
<Icon >
name="svg-spinners:180-ring-with-bg" <Icon
:size="26" name="svg-spinners:180-ring-with-bg"
class="has-text-primary" :size="26"
/> class="has-text-primary"
</div>
<div v-else-if="imeis.length === 0">
<p class="mt-6 mb-8 has-text-centered">Sản phẩm không IMEI sẵn nào.</p>
<div class="block">
<AddIMEIForm
:variant="variant"
@created="fetchImeis"
/> />
</div> </div>
<div class="block"> <div v-else-if="imeis.length === 0">
<ImportData <p class="mt-6 mb-8 has-text-centered">Sản phẩm không IMEI sẵn nào.</p>
code="imeis" <div class="block">
@close="fetchImeis" <AddIMEIForm
/> :variant="variant"
@created="fetchImeis"
/>
</div>
<div class="block">
<ImportData
code="imeis"
@close="fetchImeis"
/>
</div>
</div> </div>
</div> <template v-else>
<template v-else> <table class="table is-hoverable is-fullwidth mb-2 fs-13">
<table class="table is-hoverable is-fullwidth mb-2 fs-13"> <thead>
<thead> <tr>
<tr> <th></th>
<th></th> <th>STT</th>
<th>STT</th> <th>IMEI</th>
<th>IMEI</th> <th>Trạng thái</th>
<th>Trạng thái</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <tr
<tr v-for="(imeiRec, i) in imeis"
v-for="(imeiRec, i) in imeis" :key="imeiRec.id"
:key="imeiRec.id" class="is-clickable"
class="is-clickable" :class="selectedImeis.find((i) => i.imei === imeiRec.imei) && 'is-selected'"
:class="selectedImeis.find((i) => i.imei === imeiRec.imei) && 'is-selected'" @click="toggleSelected(imeiRec)"
@click="toggleSelected(imeiRec)" >
<td class="is-narrow">
<label class="checkbox">
<input
type="checkbox"
:checked="selectedImeis.find((i) => i.imei === imeiRec.imei)"
/>
</label>
</td>
<td class="is-narrow">{{ i + 1 }}</td>
<td class="is-family-monospace">{{ imeiRec.imei }}</td>
<td>
<span :class="['tag is-light', imeiRec.deleted ? 'is-danger' : 'is-success']">{{
imeiRec.deleted ? "Không có sẵn" : "Có sẵn"
}}</span>
</td>
</tr>
</tbody>
</table>
<div class="is-flex is-justify-content-end">
<button
@click="addToCart"
:class="['button is-primary', isAdding && 'is-loading']"
:disabled="selectedImeis.length === 0"
> >
<td class="is-narrow"> <span class="icon">
<label class="checkbox"> <Icon
<input name="material-symbols:add-rounded"
type="checkbox" :size="18"
:checked="selectedImeis.find((i) => i.imei === imeiRec.imei)" />
/> </span>
</label> <span>
</td> <span>Thêm vào giỏ</span>
<td class="is-narrow">{{ i + 1 }}</td> <span v-if="selectedImeis.length > 0"> ({{ selectedImeis.length }})</span>
<td class="is-family-monospace">{{ imeiRec.imei }}</td> </span>
<td> </button>
<span :class="['tag is-light', imeiRec.deleted ? 'is-danger' : 'is-success']">{{ </div>
imeiRec.deleted ? "Không có sẵn" : "Có sẵn" </template>
}}</span> </div>
</td>
</tr>
</tbody>
</table>
<div class="is-flex is-justify-content-end">
<button
@click="addToCart"
:class="['button is-primary', isAdding && 'is-loading']"
:disabled="selectedImeis.length === 0"
>
<span class="icon">
<Icon
name="material-symbols:add-rounded"
:size="18"
/>
</span>
<span>
<span>Thêm vào giỏ</span>
<span v-if="selectedImeis.length > 0"> ({{ selectedImeis.length }})</span>
</span>
</button>
</div>
</template>
</template> </template>
<style scoped> <style scoped>
.table tr.is-selected { .table tr.is-selected {

View File

@@ -2,7 +2,7 @@
import CartItem from "~/components/pos/CartItem.vue"; import CartItem from "~/components/pos/CartItem.vue";
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
const { $patchapi, $deleteapi, $insertapi, $dayjs, $snackbar } = useNuxtApp(); const { $id, $patchapi, $deleteapi, $insertapi, $dayjs, $snackbar } = useNuxtApp();
const id = "confirmOrder"; const id = "confirmOrder";
const isPending = ref(false); const isPending = ref(false);
const { activeCart, activeCartItems, orderInfo, getCarts } = inject("pos"); const { activeCart, activeCartItems, orderInfo, getCarts } = inject("pos");
@@ -19,10 +19,10 @@ async function createOrder() {
isPending.value = true; isPending.value = true;
const invoice = await $insertapi("Invoice", { const invoice = await $insertapi("Invoice", {
data: { data: {
customer: "", customer: activeCart.value.customer,
customer_name: "", customer_name: activeCart.value.customer__fullname,
customer_phone: "", customer_phone: activeCart.value.customer__phone,
customer_email: "", customer_email: activeCart.value.customer__email,
shipping_address: shipping_address.value, shipping_address: shipping_address.value,
product_amount: activeCartItems.value.length, product_amount: activeCartItems.value.length,
shipping_fee: 0, shipping_fee: 0,
@@ -32,7 +32,7 @@ async function createOrder() {
order_type: "pos", order_type: "pos",
status: "pending", status: "pending",
ordered_at: new Date(), ordered_at: new Date(),
paid_at: new Date(), paid_at: null,
delivery_method: orderInfo.value.deliveryMethod.id, delivery_method: orderInfo.value.deliveryMethod.id,
}, },
notify: false, notify: false,
@@ -64,6 +64,27 @@ async function createOrder() {
notify: false, notify: false,
}); });
if (orderInfo.value.deliveryMethod.code === "HOME_DELIVERY") {
const deliveryInfoPayload = {
invoice: invoice.id,
receiver_name: invoice.customer_name,
receiver_phone: invoice.customer_phone,
city: orderInfo.value.address.city,
district: orderInfo.value.address.district,
ward: orderInfo.value.address.ward,
address: orderInfo.value.address.address_detail,
shipping_fee: 0,
carrier: orderInfo.value.deliveryMethod.id,
tracking_code: $id(),
status: "pending",
};
const deliveryInfo = await $insertapi("Delivery_Info", {
data: deliveryInfoPayload,
notify: false,
});
}
$snackbar("Tạo đơn hàng thành công", "Success"); $snackbar("Tạo đơn hàng thành công", "Success");
await Promise.all([ await Promise.all([

View File

@@ -16,6 +16,24 @@ async function updateAddress() {
isLoading.value = false; isLoading.value = false;
emit("modalevent", { name: "update" }); emit("modalevent", { name: "update" });
} }
const isDirty = computed(() => {
if (props.address) {
// edit
return !isEqual(props.address, addressRef);
} else {
return true;
// create
// true if
}
});
const isValid = computed(() => {
if (!addressRef.value.city) return false;
if (!addressRef.value.district) return false;
if (!addressRef.value.ward) return false;
if (!addressRef.value.address_detail) return false;
return true;
});
</script> </script>
<template> <template>
@@ -41,7 +59,7 @@ async function updateAddress() {
v-model="addressRef.ward" v-model="addressRef.ward"
class="input" class="input"
type="text" type="text"
placeholder="Text input" placeholder="Phường"
/> />
</div> </div>
</div> </div>
@@ -54,7 +72,7 @@ async function updateAddress() {
v-model="addressRef.district" v-model="addressRef.district"
class="input" class="input"
type="text" type="text"
placeholder="Text input" placeholder="Quận"
/> />
</div> </div>
</div> </div>
@@ -67,7 +85,7 @@ async function updateAddress() {
v-model="addressRef.city" v-model="addressRef.city"
class="input" class="input"
type="text" type="text"
placeholder="Text input" placeholder="Thành phố"
/> />
</div> </div>
</div> </div>
@@ -94,6 +112,9 @@ async function updateAddress() {
</label> </label>
</div> </div>
</div> </div>
<pre>address: {{ address }}</pre>
<pre>addressRef: {{ addressRef }}</pre>
<pre>isDirty: {{ isDirty.toString() }}</pre>
<div class="field is-grouped is-grouped-right"> <div class="field is-grouped is-grouped-right">
<div class="control"> <div class="control">
<button <button
@@ -108,16 +129,16 @@ async function updateAddress() {
<button <button
type="button" type="button"
:class="['button is-link', isLoading && 'is-loading']" :class="['button is-link', isLoading && 'is-loading']"
:disabled="isEqual(address, addressRef)" :disabled="!isDirty || !isValid"
@click="updateAddress" @click="updateAddress"
> >
<span class="icon"> <span class="icon">
<Icon <Icon
name="material-symbols:edit-outline-rounded" :name="address ? 'material-symbols:edit-outline-rounded' : 'material-symbols:add-rounded'"
:size="16" :size="18"
/> />
</span> </span>
<span>Cập nhật</span> <span>{{ address ? "Cập nhật" : "Tạo" }}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -49,6 +49,14 @@ function openConfirmModal() {
}; };
} }
function openAddAddressModal() {
showModal.value = {
component: "pos/EditAddress",
title: "Thêm địa chỉ",
width: "50%",
height: "auto",
};
}
provide("pos", { provide("pos", {
carts, carts,
activeCartId, activeCartId,
@@ -188,6 +196,7 @@ provide("pos", {
column: ['name'], column: ['name'],
first: true, first: true,
placeholder: 'Phương thức giao hàng', placeholder: 'Phương thức giao hàng',
disabled: !activeCart?.customer,
optionid: orderInfo.deliveryMethod?.id, optionid: orderInfo.deliveryMethod?.id,
onOption: (e) => (orderInfo.deliveryMethod = e), onOption: (e) => (orderInfo.deliveryMethod = e),
}" }"
@@ -237,8 +246,23 @@ provide("pos", {
@update="getAddresses" @update="getAddresses"
/> />
</div> </div>
<div v-else> <div
<p class="has-text-grey has-text-centered p-4">Khách hàng chưa có địa chỉ</p> v-else
class="has-text-centered"
>
<p class="has-text-grey p-4">Khách hàng chưa có địa chỉ</p>
<button
@click="openAddAddressModal"
class="button is-light is-primary"
>
<span class="icon">
<Icon
name="material-symbols:add-home-work-outline-rounded"
:size="18"
/>
</span>
<span>Thêm địa chỉ</span>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -270,6 +294,7 @@ provide("pos", {
column: ['name'], column: ['name'],
first: true, first: true,
placeholder: 'Phương thức thanh toán', placeholder: 'Phương thức thanh toán',
disabled: !activeCart?.customer,
optionid: orderInfo.paymentMethod?.id, optionid: orderInfo.paymentMethod?.id,
onOption: (e) => (orderInfo.paymentMethod = e), onOption: (e) => (orderInfo.paymentMethod = e),
}" }"

View File

@@ -485,13 +485,20 @@ export default /** @type {const} */ ([
name: "Invoice", name: "Invoice",
url: "data/Invoice/", url: "data/Invoice/",
url_detail: "data-detail/Invoice/", url_detail: "data-detail/Invoice/",
params: {}, params: {
values:
"delivery,delivery__receiver_name,delivery__receiver_phone,delivery__tracking_code,delivery__expected_date,delivery__delivered_date,delivery__status,payments,history,id,code,customer,staff,staff__fullname,staff__phone,staff__email,voucher,customer_name,customer_phone,customer_email,shipping_address,product_amount,shipping_fee,total_amount,discount_amount,final_amount,order_type,status,note,ordered_at,paid_at,creator,updater,deleted,delivery_method,create_time,update_time",
sort: "-id",
},
}, },
{ {
name: "Invoice_Detail", name: "Invoice_Detail",
url: "data/Invoice_Detail/", url: "data/Invoice_Detail/",
url_detail: "data-detail/Invoice_Detail/", url_detail: "data-detail/Invoice_Detail/",
params: {}, params: {
values:
"id,code,invoice,variant,variant__code,variant__product,variant__product__name,variant__color,variant__price,variant__image__code,imei_sold,price,status,note,deleted,create_time,update_time",
},
}, },
{ {
name: "Manufacturer", name: "Manufacturer",
@@ -647,6 +654,12 @@ export default /** @type {const} */ ([
url_detail: "data-detail/Customer_Address/", url_detail: "data-detail/Customer_Address/",
params: {}, params: {},
}, },
{
name: "Delivery_Info",
url: "data/Delivery_Info/",
url_detail: "data-detail/Delivery_Info/",
params: {},
},
{ {
name: "Delivery_Method", name: "Delivery_Method",
url: "data/Delivery_Method/", url: "data/Delivery_Method/",