This commit is contained in:
Viet An
2026-07-01 16:54:21 +07:00
parent c2c695cbe4
commit 8d620b8bce
16 changed files with 155 additions and 157 deletions

View File

@@ -1,5 +1,4 @@
<script setup>
const { $shortenCurrency } = useNuxtApp();
const revenueChartOptions = {
chart: {
type: "spline",

View File

@@ -6,8 +6,6 @@ const props = defineProps({
order_count: Number,
paid: Number,
});
const { $shortenCurrency } = useNuxtApp();
</script>
<template>
<div class="is-flex is-gap-1 is-justify-content-space-between">

View File

@@ -5,8 +5,6 @@ const props = defineProps({
revenue: Number,
topRevenue: Number,
});
const { $shortenCurrency } = useNuxtApp();
</script>
<template>
<div>

View File

@@ -3,7 +3,7 @@ const props = defineProps({
invoice: Object,
});
const { $getdata, $dayjs, $shortenCurrency } = useNuxtApp();
const { $getdata } = useNuxtApp();
const paymentRecords = ref([]);
onMounted(async () => {
paymentRecords.value = await $getdata("Payment_Record", {
@@ -59,9 +59,12 @@ function openModal() {
</td>
<td class="has-text-centered">
<span
:class="['tag rounded-full', `has-background-${invoice.status__color}-80 has-text-${invoice.status__color}-25`]"
:class="[
'tag rounded-full',
`has-background-${invoice.invoice_status__color}-90 has-text-${invoice.invoice_status__color}-25`,
]"
>
{{ invoice.status }}
{{ invoice.invoice_status__name }}
</span>
</td>
<td>
@@ -70,8 +73,8 @@ function openModal() {
</p>
</td>
<td>
<p :class="`has-text-${invoice.delivery_status__color}-40`">
{{ invoice.delivery__status }}
<p :class="`has-text-${invoice.delivery__delivery_status__color}-40`">
{{ invoice.delivery__delivery_status__name }}
</p>
</td>
<td>

View File

@@ -29,7 +29,7 @@ const props = defineProps({
</p>
</div>
<p class="font-medium has-text-primary-50">
{{ capitalize(invoice.delivery__status) }}
{{ capitalize(invoice.delivery__delivery_status__name) }}
</p>
</div>
<div

View File

@@ -18,12 +18,9 @@ const props = defineProps({
<span class="fs-13 has-text-grey">{{ unit }}</span>
</div>
</div>
<div
:class="['p-3 is-flex', `has-background-${color}-soft`]"
style="border-radius: 8px"
>
<div :class="['p-3 is-flex rounded-lg', `has-background-${color}-soft`]">
<Icon
:name="icon"
:name="icon || 'svg-spinners:180-ring-with-bg'"
:size="26"
:class="`has-text-${color}-40`"
/>

View File

@@ -0,0 +1,43 @@
<script setup>
import { useInvoices } from "~/components/orders/composables/useInvoices";
import OrderHighlightCard from "~/components/orders/OrderHighlightCard.vue";
const store = useStore();
const { invoices, invoicesByStatus } = useInvoices();
const iconMap = {
DRAFT: "material-symbols:assignment-outline-rounded",
CONFIRMED: "material-symbols:check-circle-outline-rounded",
SHIPPING: "material-symbols:delivery-truck-speed-outline-rounded",
COMPLETED: "material-symbols:box-outline-rounded",
};
const revenue = computed(() => {
return invoices.value.reduce((prev, curr) => prev + curr.final_amount, 0);
});
</script>
<template>
<div class="fixed-grid has-2-cols-mobile has-5-cols">
<div class="grid">
<OrderHighlightCard
v-for="invoiceStatus in store.Invoice_Status"
:key="invoiceStatus.id"
v-bind="{
...invoiceStatus,
value: invoicesByStatus[invoiceStatus.code]?.length || 0,
icon: iconMap[invoiceStatus.code],
}"
/>
<OrderHighlightCard
v-bind="{
name: 'Doanh thu',
value: $shortenCurrency(revenue),
icon: 'material-symbols:attach-money-rounded',
color: 'purple',
unit: 'VNĐ',
}"
/>
</div>
</div>
</template>

View File

@@ -1,20 +1,17 @@
<script setup>
const props = defineProps({
order: Object,
invoice: Object,
});
const { $dayjs, $shortenCurrency } = useNuxtApp();
const emit = defineEmits(["selectOrder", "unselect"]);
const showModal = ref();
function openModal() {
showModal.value = {
component: "orders/SelectedOrder",
title: "SelectedOrder",
width: "75%",
height: "500px",
title: "Chi tiết đơn hàng",
width: "min(700px, 75%)",
vbind: {
order: props.order,
invoice: props.invoice,
},
};
}
@@ -22,32 +19,32 @@ function openModal() {
<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)"
:class="['card fs-14 is-clickable', `has-background-${invoice.invoice_status__color}-100`]"
:style="{ border: `1px solid var(--bulma-${invoice.invoice_status__color}-80)` }"
@click="openModal"
>
<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 }}
<p class="fs-15 font-bold">{{ invoice.code }}</p>
<span :class="['fs-13', `has-text-${invoice.payment_status__color}-40`]">
{{ invoice.payment_status__name }}
</span>
</div>
<div class="is-flex is-flex-direction-column is-gap-2">
<!-- customer info -->
<div>
<p class="has-text-grey-20">{{ order.customer__name }}</p>
<p class="has-text-grey-20">{{ invoice.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>
<p>{{ invoice.customer_phone }}</p>
</div>
</div>
<!-- product info -->
<div>
<p class="fs-24 has-text-grey-20 font-bold">
{{ $shortenCurrency(order.total) }}
{{ $shortenCurrency(invoice.final_amount) }}
</p>
<p class="fs-13 has-text-grey">{{ order.order__products.length }} sản phẩm</p>
<p class="fs-13 has-text-grey">{{ invoice.product_amount }} sản phẩm</p>
</div>
<hr class="m-0 has-background-grey-85" />
<div class="is-flex is-flex-direction-column is-gap-0.5 fs-13 has-text-grey">
@@ -56,29 +53,29 @@ function openModal() {
name="material-symbols:calendar-today-outline-rounded"
:size="16"
/>
<span>{{ $dayjs(order.create_time).format("L") }}</span>
<span>{{ $dayjs(invoice.create_time).format("L") }}</span>
<span></span>
<span>{{ $dayjs(order.create_time).format("HH:mm") }}</span>
<span>{{ $dayjs(invoice.create_time).format("HH:mm") }}</span>
</p>
<p>
NV: <span>{{ order.employee__name }}</span>
NV: <span>{{ invoice.staff__fullname }}</span>
</p>
</div>
<button
v-if="order.status__name !== 'Hoàn thành'"
v-if="invoice.invoice_status__code !== 'COMPLETED'"
:class="[
'button fs-14 has-text-white',
order.status__name === 'Nháp'
invoice.invoice_status__code === 'DRAFT'
? 'is-primary'
: order.status__name === 'Đã xác nhận'
: invoice.invoice_status__code === 'CONFIRMED'
? 'is-orange'
: 'is-success',
]"
>
{{
order.status__name === "Nháp"
invoice.invoice_status__code === "DRAFT"
? "Xác nhận"
: order.status__name === "Đã xác nhận"
: invoice.invoice_status__code === "CONFIRMED"
? "Giao hàng"
: "Hoàn thành"
}}

View File

@@ -1,36 +1,9 @@
<script setup>
import { useInvoices } from "~/components/orders/composables/useInvoices";
import PipelinePhase from "~/components/orders/PipelinePhase.vue";
const phases = [
{
name: "Nháp",
value: 2,
icon: "material-symbols:assignment-outline-rounded",
color: "yellow",
index: 1,
},
{
name: "Đã xác nhận",
value: 3,
icon: "material-symbols:check-circle-outline-rounded",
color: "blue",
index: 2,
},
{
name: "Đang giao",
value: 2,
icon: "material-symbols:delivery-truck-speed-outline-rounded",
color: "orange",
index: 3,
},
{
name: "Hoàn thành",
value: 1,
icon: "material-symbols:box-outline-rounded",
color: "green",
index: 4,
},
];
const store = useStore();
const { invoices } = useInvoices();
</script>
<template>
<div class="card">
@@ -38,16 +11,16 @@ const phases = [
<p class="fs-16 font-semibold mb-4">Pipeline đơn hàng</p>
<div class="is-flex is-justify-content-space-evenly">
<PipelinePhase
v-for="phase in phases"
:key="phases.name"
v-bind="phase"
v-for="invoiceStatus in store.Invoice_Status"
:key="invoiceStatus.id"
v-bind="invoiceStatus"
/>
</div>
<hr class="has-background-grey-85" />
<div class="is-flex is-justify-content-space-between">
<p>Tổng đơn hàng</p>
<p class="fs-18 font-semibold">
{{ phases.reduce((prev, curr) => prev + curr.value, 0) }}
{{ invoices.length }}
</p>
</div>
</div>

View File

@@ -1,42 +1,10 @@
<script setup>
import OrderHighlightCard from "~/components/orders/OrderHighlightCard.vue";
import { useInvoices } from "~/components/orders/composables/useInvoices";
import OrderHighlights from "~/components/orders/OrderHighlights.vue";
import OrderPipeline from "~/components/orders/OrderPipeline.vue";
import OrdersTable from "~/components/orders/OrdersTable.vue";
const { $getdata, $store } = useNuxtApp();
const highlights = [
{
name: "Nháp",
value: 2,
icon: "material-symbols:assignment-outline-rounded",
color: "yellow",
},
{
name: "Đã xác nhận",
value: 3,
icon: "material-symbols:check-circle-outline-rounded",
color: "blue",
},
{
name: "Đang giao",
value: 2,
icon: "material-symbols:delivery-truck-speed-outline-rounded",
color: "orange",
},
{
name: "Hoàn thành",
value: 1,
icon: "material-symbols:box-outline-rounded",
color: "green",
},
{
name: "Doanh thu",
value: "6.8M",
icon: "material-symbols:attach-money-rounded",
color: "purple",
unit: "VNĐ",
},
];
const store = useStore();
const viewModes = [
{ name: "list", icon: "material-symbols:format-list-bulleted-rounded" },
@@ -44,20 +12,15 @@ const viewModes = [
];
const viewMode = ref("list");
const invoices = ref();
onMounted(async () => {
invoices.value = await $getdata("Invoice");
});
provide("orders", {
invoices,
});
const { fetchInvoices } = useInvoices();
onMounted(fetchInvoices);
</script>
<template>
<div>
<Teleport
defer
to="#header-right-slot"
v-if="$store.tabinfo.tab.code === 'orders'"
v-if="store.tabinfo.tab.code === 'orders'"
>
<div class="is-flex is-gap-2 is-justify-content-flex-end is-align-items-center">
<div class="tabs is-toggle m-0">
@@ -90,15 +53,7 @@ provide("orders", {
</button>
</div>
</Teleport>
<div class="fixed-grid has-2-cols-mobile has-5-cols">
<div class="grid">
<OrderHighlightCard
v-for="highlight in highlights"
:key="highlight.name"
v-bind="highlight"
/>
</div>
</div>
<OrderHighlights />
<OrderPipeline />
<OrdersTable :viewMode="viewMode" />
</div>

View File

@@ -6,14 +6,14 @@ const props = defineProps({
statuses: Array,
});
const emit = defineEmits(["selectOrder", "unselect"]);
const store = useStore();
</script>
<template>
<div class="p-5 fixed-grid has-4-cols">
<div class="grid">
<div
v-for="status in statuses"
v-for="status in store.Invoice_Status"
:key="status.id"
class="card"
style="border: none"
@@ -30,18 +30,16 @@ const emit = defineEmits(["selectOrder", "unselect"]);
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 }}
{{ orders.filter((o) => o.invoice_status === status.id).length }}
</p>
</div>
<hr class="m-0 has-background-grey-85" />
<div class="has-background-grey-100 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)"
v-if="orders.filter((o) => o.invoice_status === status.id).length > 0"
v-for="order in orders.filter((o) => o.invoice_status === status.id)"
:key="order.id"
:order="order"
@selectOrder="emit('selectOrder', order.id)"
@unselect="emit('unselect')"
:invoice="order"
/>
<p
v-else

View File

@@ -1,5 +1,6 @@
<script setup>
import { pull } from "es-toolkit";
import { useInvoices } from "~/components/orders/composables/useInvoices";
import InvoiceRow from "~/components/orders/InvoiceRow.vue";
import OrdersKanban from "~/components/orders/OrdersKanban.vue";
@@ -7,8 +8,9 @@ const props = defineProps({
viewMode: String,
});
const { $findapi, $getapi, $dayjs } = useNuxtApp();
const { invoices } = inject("orders");
const { $getdata, $dayjs } = useNuxtApp();
const { invoices, isPending } = useInvoices();
const store = useStore();
const statuses = [
{
@@ -33,15 +35,10 @@ const statuses = [
},
];
const paymentStatuses = ref([]);
const employees = ref([]);
onMounted(async () => {
const apis = $findapi(["Payment_Status", "Staff"]);
const [paymentStatusRes, staffRes] = await $getapi(apis);
paymentStatuses.value = paymentStatusRes.data.rows || [];
employees.value = staffRes.data.rows || [];
employees.value = await $getdata("Staff");
});
const input = ref();
@@ -65,7 +62,7 @@ const filteredInvoices = computed(() => {
const filteredByStatuses = filteredByInput.filter((order) => {
if (selectedStatuses.value.length === 0) return true;
return selectedStatuses.value.includes(order.status);
return selectedStatuses.value.includes(order.invoice_status);
});
const filteredByPaymentStatuses = filteredByStatuses.filter((order) => {
@@ -148,7 +145,7 @@ function toggleStatus(id) {
<select v-model="selectedPaymentStatus">
<option :value="undefined">Thanh toán</option>
<option
v-for="paymentStatus in paymentStatuses"
v-for="paymentStatus in store.Payment_Status"
:key="paymentStatus.id"
:value="paymentStatus.id"
>
@@ -171,7 +168,7 @@ function toggleStatus(id) {
</div>
<div class="is-flex is-gap-1 mt-3">
<button
v-for="status in statuses"
v-for="status in store.Invoice_Status"
:key="status.id"
:class="[
'tag fs-13 is-rounded',
@@ -218,6 +215,15 @@ function toggleStatus(id) {
</tr>
</thead>
<tbody>
<tr v-if="isPending">
<td colspan="8">
<Icon
name="svg-spinners:180-ring-with-bg"
:size="20"
class="has-text-primary is-block mx-auto"
/>
</td>
</tr>
<InvoiceRow
v-for="invoice in filteredInvoices"
:key="invoice.id"

View File

@@ -1,28 +1,39 @@
<script setup>
import { useInvoices } from "~/components/orders/composables/useInvoices";
const props = defineProps({
id: Number,
code: String,
name: String,
value: Number,
icon: String,
en: String,
color: String,
index: Number,
});
const { invoices, invoicesByStatus } = useInvoices();
const invoiceMatchCount = computed(() => {
if (!props.code) return 0;
if (!invoicesByStatus.value[props.code]) return 0;
return invoicesByStatus.value[props.code].length;
});
const progressVal = computed(() => (invoiceMatchCount.value * 100) / (invoices.value.length || 1));
const progressUnfilled = computed(() => `var(--bulma-${props.color}-85)`);
</script>
<template>
<div class="is-flex-grow-1">
<div class="is-flex is-justify-content-space-between">
<p :class="`has-text-${color}-40`">{{ name }}</p>
<p :class="['fs-18 font-bold', `has-text-${color}-30`]">{{ value }}</p>
<p :class="['fs-18 font-bold', `has-text-${color}-30`]">{{ invoiceMatchCount }}</p>
</div>
<progress
:class="['progress is-small mt-2', `is-${color}`]"
value="20"
:value="progressVal"
max="100"
></progress>
</div>
<div
v-if="index < 4"
v-if="index < 3"
class="is-flex is-justify-content-center is-align-items-center"
style="width: 60px; height: 48px"
>

View File

@@ -51,10 +51,10 @@ const activeTab = ref(tabs[0]);
<span
:class="[
'tag rounded-full',
`has-background-${invoice.status__color}-80 has-text-${invoice.status__color}-25`,
`has-background-${invoice.invoice_status__color}-80 has-text-${invoice.invoice_status__color}-25`,
]"
>
{{ invoice.status }}
{{ invoice.invoice_status__name }}
</span>
</div>
<div class="is-flex is-gap-0.5 is-flex-direction-column mt-4">

View File

@@ -0,0 +1,20 @@
import { groupBy } from "es-toolkit";
const invoices = ref([]);
const isPending = ref(false);
export function useInvoices() {
const { $getdata } = useNuxtApp();
async function fetchInvoices() {
isPending.value = true;
invoices.value = await $getdata("Invoice");
isPending.value = false;
}
const invoicesByStatus = computed(() => {
return groupBy(invoices.value, (invoice) => invoice.invoice_status__code);
});
return { invoices, isPending, fetchInvoices, invoicesByStatus };
}

View File

@@ -487,7 +487,7 @@ export default /** @type {const} */ ([
url_detail: "data-detail/Invoice/",
params: {
values:
"delivery,delivery__receiver_name,delivery__receiver_phone,delivery__tracking_code,delivery__expected_date,delivery__delivered_date,delivery__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,invoice_status,invoice_status__code,invoice_status__name,invoice_status__color,payment_status,payment_status__code,payment_status__name,payment_status__color,note,ordered_at,paid_at,creator,updater,deleted,delivery_method,create_time,update_time",
"delivery,delivery__receiver_name,delivery__receiver_phone,delivery__tracking_code,delivery__expected_date,delivery__delivered_date,delivery__delivery_status,delivery__delivery_status__code,delivery__delivery_status__name,delivery__delivery_status__color,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,invoice_status,invoice_status__code,invoice_status__name,invoice_status__color,payment_status,payment_status__code,payment_status__name,payment_status__color,note,ordered_at,paid_at,creator,updater,deleted,delivery_method,create_time,update_time",
sort: "-id",
},
},
@@ -505,7 +505,7 @@ export default /** @type {const} */ ([
commit: "Invoice_Status",
url: "data/Invoice_Status/",
url_detail: "data-detail/Invoice_Status/",
params: {},
params: { sort: "index" },
},
{
name: "Manufacturer",
@@ -666,7 +666,7 @@ export default /** @type {const} */ ([
commit: "Payment_Status",
url: "data/Payment_Status/",
url_detail: "data-detail/Payment_Status/",
params: {},
params: { sort: "index" },
},
{
name: "Customer_Address",
@@ -691,7 +691,7 @@ export default /** @type {const} */ ([
commit: "Delivery_Status",
url: "data/Delivery_Status/",
url_detail: "data-detail/Delivery_Status/",
params: {},
params: { sort: "index" },
},
{
name: "Cart",