Merge branch 'develop'
This commit is contained in:
commit
2f17e8a35d
14 changed files with 686 additions and 17 deletions
|
|
@ -1,7 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { formatNumberDecimal } from 'src/stores/utils';
|
||||
|
||||
defineEmits<{ (e: 'labelClick', value: string, index: number): void }>();
|
||||
defineEmits<{ (e: 'labelClick', value: string, index: number | null): void }>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
|
@ -38,7 +39,7 @@ withDefaults(
|
|||
v-if="typeof value === 'string'"
|
||||
@click="$emit('labelClick', value, null)"
|
||||
>
|
||||
{{ value }}
|
||||
{{ formatNumberDecimal(+value, 2) }}
|
||||
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
|
||||
</span>
|
||||
<span v-else :class="{ 'link cursor-pointer': clickable }">
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export default {
|
|||
group: 'Group',
|
||||
laborIdentified: 'Labor identified',
|
||||
beDue: 'Due in',
|
||||
noReason: 'No reason',
|
||||
due: 'Due',
|
||||
overDue: 'Overdue',
|
||||
status: 'Status',
|
||||
|
|
@ -936,6 +937,7 @@ export default {
|
|||
InProgress: 'In Progress',
|
||||
Completed: 'Completed',
|
||||
Canceled: 'Canceled',
|
||||
CancelRequested: 'Cancel Requested',
|
||||
|
||||
AwaitOrder: 'Awaiting Order',
|
||||
ReadyOrder: 'Ready for Order',
|
||||
|
|
@ -1336,7 +1338,6 @@ export default {
|
|||
Succeed: 'Completed',
|
||||
},
|
||||
},
|
||||
|
||||
report: {
|
||||
report: {
|
||||
title: 'Report',
|
||||
|
|
@ -1373,4 +1374,36 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
newComer: 'New Interested Party',
|
||||
openQuotation: 'Open Quotation',
|
||||
paidSales: 'Paid Sales',
|
||||
pendingSales: 'Pending Sales',
|
||||
|
||||
receipt: {
|
||||
title: 'Tax Invoice Summary',
|
||||
caption: 'By Total Amount',
|
||||
total: 'Total Amount',
|
||||
paid: 'Partially & Fully Paid',
|
||||
pending: 'Pending Payment',
|
||||
cancel: 'Cancelled',
|
||||
},
|
||||
opportunity: {
|
||||
title: 'Sales Opportunity Screening',
|
||||
caption: 'By Sales Opportunity Status',
|
||||
},
|
||||
quotation: {
|
||||
title: 'Quotations & Sales Orders',
|
||||
caption: 'By Document Status',
|
||||
waitCustomer: 'Waiting for Customer',
|
||||
inProgress: 'In Progress',
|
||||
complete: 'Complete',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
sales: {
|
||||
title: 'Top 5 Highest Sales',
|
||||
caption: 'Based on Tax Invoices',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export default {
|
|||
group: 'กลุ่ม',
|
||||
laborIdentified: 'ระบุแรงงาน',
|
||||
beDue: 'จะครบกำหนดในอีก',
|
||||
noReason: 'ไม่มีเหตุผล',
|
||||
due: 'ครบกำหนด',
|
||||
overDue: 'เลยกำหนด',
|
||||
status: 'สถานะ',
|
||||
|
|
@ -924,6 +925,7 @@ export default {
|
|||
InProgress: 'กำลังดำเนินการ',
|
||||
Completed: 'เสร็จสิ้น',
|
||||
Canceled: 'ยกเลิก',
|
||||
CancelRequested: 'ต้องการยกเลิก',
|
||||
|
||||
AwaitOrder: 'รอสั่งงาน',
|
||||
ReadyOrder: 'พร้อมสั่งงาน',
|
||||
|
|
@ -1351,4 +1353,36 @@ export default {
|
|||
count: 'จำนวนที่ขาย',
|
||||
},
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
newComer: 'ผู้สนใจใหม่',
|
||||
openQuotation: 'ใบเสนอราคาที่เปิดอยู่',
|
||||
paidSales: 'ยอดขายที่ชำระแล้ว',
|
||||
pendingSales: 'ยอดขายที่รอชำระ',
|
||||
|
||||
receipt: {
|
||||
title: 'สรุปยอดใบกำกับภาษี',
|
||||
caption: 'ตามจำนวนเงินรวม',
|
||||
total: 'ยอดรวมทั้งหมด',
|
||||
paid: 'ชำระแล้ว',
|
||||
pending: 'รอชำระ',
|
||||
cancel: 'ยกเลิก',
|
||||
},
|
||||
opportunity: {
|
||||
title: 'การคัดกรองโอกาสทางการขาย',
|
||||
caption: 'ตามสถานะโอกาสทางการขาย',
|
||||
},
|
||||
quotation: {
|
||||
title: 'ใบเสนอราคาและใบสั่งขาย',
|
||||
caption: 'ตามสถานะเอกสาร',
|
||||
waitCustomer: 'รอลูกค้าตอบรับ',
|
||||
inProgress: 'กำลังดำเนินการ',
|
||||
complete: 'เสร็จสิ้น',
|
||||
cancel: 'ยกเลิก',
|
||||
},
|
||||
sales: {
|
||||
title: '5 อันดับยอดขายสูงสุด',
|
||||
caption: 'ตามใบกำกับภาษี',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { baseUrl } from 'src/stores/utils';
|
||||
|
||||
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||
|
||||
import { ProductRelation, PayCondition } from 'src/stores/quotations/types';
|
||||
import { Step, RequestWorkStatus } from 'src/stores/request-list/types';
|
||||
|
||||
const workStatus = ref([
|
||||
RequestWorkStatus.Ready,
|
||||
RequestWorkStatus.Waiting,
|
||||
RequestWorkStatus.InProgress,
|
||||
RequestWorkStatus.Validate,
|
||||
RequestWorkStatus.Ended,
|
||||
RequestWorkStatus.Completed,
|
||||
]);
|
||||
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||
|
||||
defineEmits<{
|
||||
(
|
||||
|
|
@ -29,6 +18,8 @@ const props = defineProps<{
|
|||
code: string;
|
||||
status?: Step;
|
||||
imgUrl?: string;
|
||||
requestCancel?: boolean;
|
||||
requestCancelReason?: string;
|
||||
installmentInfo?: {
|
||||
total: number;
|
||||
paid?: number;
|
||||
|
|
@ -108,6 +99,18 @@ function changeableStatus(currentStatus?: RequestWorkStatus) {
|
|||
<div class="rounded q-px-xs app-text-muted surface-3">
|
||||
{{ product?.code || code }}
|
||||
</div>
|
||||
<BadgeComponent
|
||||
v-if="requestCancel && !cancel"
|
||||
:hsla-color="'--red-5-hsl'"
|
||||
class="q-ml-sm"
|
||||
:title="$t(`requestList.status.CancelRequested`) || '-'"
|
||||
>
|
||||
<template #append>
|
||||
<q-tooltip>
|
||||
{{ requestCancelReason || $t('general.noReason') }}
|
||||
</q-tooltip>
|
||||
</template>
|
||||
</BadgeComponent>
|
||||
</div>
|
||||
|
||||
<div class="q-ml-auto q-gutter-y-xs">
|
||||
|
|
|
|||
|
|
@ -731,6 +731,8 @@ async function submitRequestAction(data: {
|
|||
<!-- product -->
|
||||
<template v-for="(value, index) in productsList" :key="value">
|
||||
<ProductExpansion
|
||||
:request-cancel="value.customerRequestCancel"
|
||||
:request-cancel-reason="value.customerRequestCancelReason"
|
||||
:cancel="data.requestDataStatus === RequestDataStatus.Canceled"
|
||||
:readonly="
|
||||
data.requestDataStatus === RequestDataStatus.Canceled ||
|
||||
|
|
|
|||
|
|
@ -183,6 +183,24 @@ function getEmployeeName(
|
|||
$t(`requestList.status.${props.row.requestDataStatus}`) || '-'
|
||||
"
|
||||
/>
|
||||
<BadgeComponent
|
||||
v-if="
|
||||
props.row.customerRequestCancel &&
|
||||
props.row.requestDataStatus !== RequestDataStatus.Canceled
|
||||
"
|
||||
:hsla-color="'--red-5-hsl'"
|
||||
class="q-ml-sm"
|
||||
:title="$t(`requestList.status.CancelRequested`) || '-'"
|
||||
>
|
||||
<template #append>
|
||||
<q-tooltip>
|
||||
{{
|
||||
props.row.customerRequestCancelReason ||
|
||||
$t('general.noReason')
|
||||
}}
|
||||
</q-tooltip>
|
||||
</template>
|
||||
</BadgeComponent>
|
||||
</q-td>
|
||||
<q-td class="text-right">
|
||||
<q-btn
|
||||
|
|
|
|||
|
|
@ -1,11 +1,209 @@
|
|||
<script lang="ts" setup>
|
||||
// NOTE: Library
|
||||
import { onMounted, reactive } from 'vue';
|
||||
|
||||
// NOTE: Components
|
||||
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
import ChartReceipt from './chart/ChartReceipt.vue';
|
||||
import ChartOpportunity from './chart/ChartOpportunity.vue';
|
||||
import ChartQuotationStatus from './chart/ChartQuotationStatus.vue';
|
||||
import ChartSales from './chart/ChartSales.vue';
|
||||
|
||||
// NOTE: Stores & Type
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import { useQuotationStore } from 'src/stores/quotations';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useReportStore } from 'src/stores/report';
|
||||
import { PaymentDataStatus } from 'src/stores/payment/types';
|
||||
|
||||
// NOTE: Variable
|
||||
const navigatorStore = useNavigator();
|
||||
const quotationStore = useQuotationStore();
|
||||
const reportStore = useReportStore();
|
||||
const { stats: quotationStats } = storeToRefs(quotationStore);
|
||||
const { dataReportPayment } = storeToRefs(reportStore);
|
||||
|
||||
const state = reactive({
|
||||
role: 'admin',
|
||||
year: 'now',
|
||||
});
|
||||
|
||||
// NOTE: mock
|
||||
const option = reactive({
|
||||
role: [
|
||||
{ label: 'ผู้ดูแล', value: 'admin' },
|
||||
{ label: 'ผู้ใช้', value: 'user' },
|
||||
],
|
||||
year: [
|
||||
{ label: 'ปีนี้', value: 'now' },
|
||||
{ label: '2024', value: '2024' },
|
||||
],
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
navigatorStore.current.title = 'dashboard.title';
|
||||
navigatorStore.current.path = [{ text: '' }];
|
||||
|
||||
const ret = await quotationStore.getQuotationStats();
|
||||
if (ret) {
|
||||
quotationStats.value = Object.assign(quotationStats.value, ret);
|
||||
console.log(quotationStats.value);
|
||||
}
|
||||
|
||||
const retPayment = await reportStore.getReportPayment();
|
||||
|
||||
if (retPayment) {
|
||||
dataReportPayment.value = retPayment;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template></template>
|
||||
<template>
|
||||
<div class="column full-height no-wrap">
|
||||
<header class="row q-gutter-sm">
|
||||
<SelectInput
|
||||
class="col-md-3 col"
|
||||
:option="option.role"
|
||||
v-model="state.role"
|
||||
></SelectInput>
|
||||
<SelectInput
|
||||
class="col-md-2 col"
|
||||
:option="option.year"
|
||||
v-model="state.year"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-calendar-outline app-text-muted" />
|
||||
</template>
|
||||
</SelectInput>
|
||||
|
||||
<div class="col-12 scroll">
|
||||
<div style="display: inline-block">
|
||||
<StatCardComponent
|
||||
:dark="$q.dark.isActive"
|
||||
text-size="10px"
|
||||
:branch="[
|
||||
{
|
||||
icon: 'material-symbols-light:receipt-long',
|
||||
count:
|
||||
(quotationStats.issued || 0) +
|
||||
(quotationStats.accepted || 0) +
|
||||
(quotationStats.paymentInProcess || 0) +
|
||||
(quotationStats.paymentSuccess || 0) +
|
||||
(quotationStats.processComplete || 0) +
|
||||
(quotationStats.canceled || 0),
|
||||
label: $t('dashboard.openQuotation'),
|
||||
color: 'cyan',
|
||||
},
|
||||
{
|
||||
icon: 'si:wallet-detailed-line',
|
||||
count:
|
||||
(quotationStats.paymentSuccess || 0) +
|
||||
(quotationStats.processComplete || 0),
|
||||
label: $t('dashboard.paidSales'),
|
||||
color: 'light-yellow',
|
||||
},
|
||||
{
|
||||
icon: 'fluent:money-hand-24-regular',
|
||||
count:
|
||||
(quotationStats.issued || 0) +
|
||||
(quotationStats.accepted || 0) +
|
||||
(quotationStats.paymentInProcess || 0),
|
||||
label: $t('dashboard.pendingSales'),
|
||||
color: 'light-purple',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="col q-mt-md">
|
||||
<div class="full-height scroll">
|
||||
<article class="row q-col-gutter-sm">
|
||||
<div class="col-sm-8 col-12">
|
||||
<ChartReceipt
|
||||
class="full-height"
|
||||
:summary="[
|
||||
dataReportPayment.reduce(
|
||||
(a, c) =>
|
||||
a +
|
||||
(c.data[PaymentDataStatus.Success] || 0) +
|
||||
(c.data[PaymentDataStatus.Wait] || 0),
|
||||
0,
|
||||
),
|
||||
dataReportPayment.reduce(
|
||||
(a, c) => a + (c.data[PaymentDataStatus.Success] || 0),
|
||||
0,
|
||||
),
|
||||
dataReportPayment.reduce(
|
||||
(a, c) => a + (c.data[PaymentDataStatus.Wait] || 0),
|
||||
0,
|
||||
),
|
||||
]"
|
||||
:categories="
|
||||
dataReportPayment.map((v) => v.month + ' / ' + v.year)
|
||||
"
|
||||
:series="[
|
||||
{
|
||||
name: $t('dashboard.receipt.total'),
|
||||
data: dataReportPayment.map(
|
||||
(v) =>
|
||||
(v.data[PaymentDataStatus.Success] || 0) +
|
||||
(v.data[PaymentDataStatus.Wait] || 0),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: $t('dashboard.receipt.paid'),
|
||||
data: dataReportPayment.map(
|
||||
(v) => v.data[PaymentDataStatus.Success] || 0,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: $t('dashboard.receipt.pending'),
|
||||
data: dataReportPayment.map(
|
||||
(v) => v.data[PaymentDataStatus.Wait] || 0,
|
||||
),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="col-sm-4 col-12">
|
||||
<ChartOpportunity
|
||||
class="full-height"
|
||||
:labels="['1', '2', '3', '4', '5', '6', '7', '8']"
|
||||
:series="[581, 389, 609, 581, 603, 600, 699, 347]"
|
||||
/>
|
||||
</div> -->
|
||||
<div class="col-sm-4 col-12">
|
||||
<ChartQuotationStatus
|
||||
v-if="quotationStats"
|
||||
class="full-height"
|
||||
:labels="[
|
||||
$t('dashboard.quotation.waitCustomer'),
|
||||
$t('dashboard.quotation.inProgress'),
|
||||
$t('dashboard.quotation.complete'),
|
||||
$t('dashboard.quotation.cancel'),
|
||||
]"
|
||||
:series="[
|
||||
{
|
||||
data: [
|
||||
(quotationStats.issued || 0) +
|
||||
(quotationStats.accepted || 0),
|
||||
(quotationStats.paymentInProcess || 0) +
|
||||
(quotationStats.paymentSuccess || 0),
|
||||
quotationStats.processComplete || 0,
|
||||
quotationStats.canceled || 0,
|
||||
],
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="col-sm-8 col-12">
|
||||
<ChartSales class="full-height" />
|
||||
</div> -->
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
58
src/pages/15_dash-board/chart/ChartOpportunity.vue
Normal file
58
src/pages/15_dash-board/chart/ChartOpportunity.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
series: number[];
|
||||
labels: string[];
|
||||
}>(),
|
||||
{
|
||||
labels: () => ['series-1', 'series-2', 'series-3', 'series-4'],
|
||||
series: () => [1, 1, 1, 1],
|
||||
},
|
||||
);
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
labels: props.labels,
|
||||
colors: [
|
||||
'#1F253B',
|
||||
'#1B285C',
|
||||
'#031B7D',
|
||||
'#0023AE',
|
||||
'#0933DA',
|
||||
'#365AE9',
|
||||
'#6380F8',
|
||||
'#7583BB',
|
||||
],
|
||||
chart: {
|
||||
fontFamily: 'Noto Sans Thai',
|
||||
type: 'donut',
|
||||
},
|
||||
dataLabels: {
|
||||
style: {
|
||||
fontSize: '10px',
|
||||
color: 'red',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
position: 'bottom',
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
<template>
|
||||
<div class="surface-1 rounded bordered q-pa-md">
|
||||
{{ $t('dashboard.opportunity.title') }}
|
||||
<div class="text-caption app-text-muted">
|
||||
{{ $t('dashboard.opportunity.caption') }}
|
||||
</div>
|
||||
<VueApexCharts
|
||||
type="donut"
|
||||
height="200"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
></VueApexCharts>
|
||||
</div>
|
||||
</template>
|
||||
107
src/pages/15_dash-board/chart/ChartQuotationStatus.vue
Normal file
107
src/pages/15_dash-board/chart/ChartQuotationStatus.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
series: { data: number[] }[];
|
||||
labels: string[];
|
||||
}>(),
|
||||
{
|
||||
series: () => [
|
||||
{
|
||||
data: [1, 2, 3, 4],
|
||||
},
|
||||
],
|
||||
labels: () => ['1', '2', '3', '4'],
|
||||
},
|
||||
);
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
chart: {
|
||||
type: 'bar',
|
||||
fontFamily: 'Noto Sans Thai',
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
barHeight: '100%',
|
||||
distributed: true,
|
||||
horizontal: true,
|
||||
borderRadius: 6,
|
||||
borderRadiusApplication: 'end',
|
||||
dataLabels: {
|
||||
position: 'bottom',
|
||||
},
|
||||
},
|
||||
},
|
||||
colors: ['#f59f00', '#035aa1', '#0ca678', '#c92a2a'],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
textAnchor: 'start',
|
||||
style: {
|
||||
colors: ['#fff'],
|
||||
},
|
||||
formatter: function (
|
||||
val: string,
|
||||
opt: {
|
||||
w: { globals: { labels: { [x: string]: string } } };
|
||||
dataPointIndex: string | number;
|
||||
},
|
||||
) {
|
||||
return opt.w.globals.labels[opt.dataPointIndex] + ': ' + val;
|
||||
},
|
||||
offsetX: 0,
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 1,
|
||||
colors: ['#fff'],
|
||||
},
|
||||
xaxis: {
|
||||
categories: props.labels,
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark',
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
formatter: function (
|
||||
val: string,
|
||||
opt: {
|
||||
w: { globals: { labels: { [x: string]: string } } };
|
||||
dataPointIndex: string | number;
|
||||
},
|
||||
) {
|
||||
return opt.w.globals.labels[opt.dataPointIndex] + ':';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
<template>
|
||||
<div class="surface-1 rounded bordered q-pa-md">
|
||||
{{ $t('dashboard.quotation.title') }}
|
||||
<div class="text-caption app-text-muted">
|
||||
{{ $t('dashboard.quotation.caption') }}
|
||||
</div>
|
||||
<VueApexCharts
|
||||
type="bar"
|
||||
height="200"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
></VueApexCharts>
|
||||
</div>
|
||||
</template>
|
||||
111
src/pages/15_dash-board/chart/ChartReceipt.vue
Normal file
111
src/pages/15_dash-board/chart/ChartReceipt.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { locale } = useI18n({ useScope: 'global' });
|
||||
|
||||
const prop = withDefaults(
|
||||
defineProps<{
|
||||
series: { name: string; data: number[] }[];
|
||||
summary: number[];
|
||||
categories: string[];
|
||||
}>(),
|
||||
{
|
||||
series: () => [
|
||||
{ name: 'series-1', data: [0, 0] },
|
||||
{ name: 'series-2', data: [1, 1] },
|
||||
{ name: 'series-3', data: [2, 2] },
|
||||
],
|
||||
summary: () => [0, 0, 0, 0],
|
||||
categories: () => ['Jan', 'Feb', 'Mar'],
|
||||
},
|
||||
);
|
||||
const chartOptions = computed(() => {
|
||||
const thaiMonths = [
|
||||
'ม.ค.',
|
||||
'ก.พ.',
|
||||
'มี.ค.',
|
||||
'เม.ย.',
|
||||
'พ.ค.',
|
||||
'มิ.ย.',
|
||||
'ก.ค.',
|
||||
'ส.ค.',
|
||||
'ก.ย.',
|
||||
'ต.ค.',
|
||||
'พ.ย.',
|
||||
'ธ.ค.',
|
||||
];
|
||||
|
||||
return {
|
||||
colors: ['#035aa1', '#ae3ec9', '#ffa94d', '#e64980'],
|
||||
chart: {
|
||||
fontFamily: 'Noto Sans Thai',
|
||||
type: 'line',
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: 'right',
|
||||
},
|
||||
xaxis: {
|
||||
categories: prop.categories,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const detail = [
|
||||
{
|
||||
label: 'total',
|
||||
color: 'var(--brand-1)',
|
||||
icon: 'hugeicons:wallet-03',
|
||||
},
|
||||
{
|
||||
label: 'paid',
|
||||
color: 'var(--purple-7)',
|
||||
icon: 'material-symbols-light:credit-card-clock-outline-rounded',
|
||||
},
|
||||
{
|
||||
label: 'pending',
|
||||
color: 'var(--orange-4)',
|
||||
icon: 'material-symbols-light:credit-card-outline',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div class="surface-1 rounded bordered q-pa-md">
|
||||
{{ $t('dashboard.receipt.title') }}
|
||||
<div class="text-caption app-text-muted">
|
||||
{{ $t('dashboard.receipt.caption') }}
|
||||
</div>
|
||||
<VueApexCharts
|
||||
type="line"
|
||||
height="150"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
></VueApexCharts>
|
||||
|
||||
<div class="row items-center">
|
||||
<span
|
||||
class="col-md col-6 row no-wrap items-center"
|
||||
v-for="(v, i) in detail"
|
||||
:key="v.label"
|
||||
>
|
||||
<q-avatar
|
||||
class="q-mr-sm"
|
||||
size="md"
|
||||
font-size="18px"
|
||||
:style="`background-color: ${v.color}; color: var(--surface-1)`"
|
||||
>
|
||||
<Icon :icon="v.icon" />
|
||||
</q-avatar>
|
||||
<DataDisplay
|
||||
:label="$t(`dashboard.receipt.${v.label}`)"
|
||||
:value="summary[i].toString()"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
75
src/pages/15_dash-board/chart/ChartSales.vue
Normal file
75
src/pages/15_dash-board/chart/ChartSales.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Net Profit',
|
||||
data: [44, 55, 57, 56, 61, 58, 63, 60, 66],
|
||||
},
|
||||
{
|
||||
name: 'Revenue',
|
||||
data: [76, 85, 101, 98, 87, 105, 91, 114, 94],
|
||||
},
|
||||
{
|
||||
name: 'Free Cash Flow',
|
||||
data: [35, 41, 36, 26, 45, 48, 52, 53, 41],
|
||||
},
|
||||
];
|
||||
const chartOptions = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
fontFamily: 'Noto Sans Thai',
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '55%',
|
||||
borderRadius: 2,
|
||||
borderRadiusApplication: 'end',
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 2,
|
||||
colors: ['transparent'],
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'],
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: '$ (thousands)',
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: function (val: string) {
|
||||
return '$ ' + val + ' thousands';
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="surface-1 rounded bordered q-pa-md">
|
||||
{{ $t('dashboard.sales.title') }}
|
||||
<div class="text-caption app-text-muted">
|
||||
{{ $t('dashboard.sales.caption') }}
|
||||
</div>
|
||||
<VueApexCharts
|
||||
type="bar"
|
||||
height="200"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
></VueApexCharts>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,7 +2,13 @@ import { ref } from 'vue';
|
|||
import { defineStore } from 'pinia';
|
||||
import { Pagination, Status } from '../types';
|
||||
import { api } from 'src/boot/axios';
|
||||
import { Report, ReportProduct, ReportQuotation, ReportSale } from './types';
|
||||
import {
|
||||
Report,
|
||||
ReportPayment,
|
||||
ReportProduct,
|
||||
ReportQuotation,
|
||||
ReportSale,
|
||||
} from './types';
|
||||
|
||||
const ENDPOINT = 'report';
|
||||
|
||||
|
|
@ -48,12 +54,21 @@ export async function getReportProduct() {
|
|||
return null;
|
||||
}
|
||||
|
||||
export async function getReportPayment() {
|
||||
const res = await api.get<ReportPayment[]>(`/${ENDPOINT}/payment`);
|
||||
if (res.status < 400) {
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const useReportStore = defineStore('report-store', () => {
|
||||
const dataReportQuotation = ref<ReportQuotation[]>([]);
|
||||
const dataReportInvoice = ref<Report[]>([]);
|
||||
const dataReportReceipt = ref<Report[]>([]);
|
||||
const dataReportSale = ref<ReportSale>();
|
||||
const dataReportProduct = ref<ReportProduct[]>([]);
|
||||
const dataReportPayment = ref<ReportPayment[]>([]);
|
||||
|
||||
return {
|
||||
dataReportQuotation,
|
||||
|
|
@ -61,11 +76,13 @@ export const useReportStore = defineStore('report-store', () => {
|
|||
dataReportReceipt,
|
||||
dataReportSale,
|
||||
dataReportProduct,
|
||||
dataReportPayment,
|
||||
|
||||
getReportQuotation,
|
||||
getReportInvoice,
|
||||
getReportReceipt,
|
||||
getReportSale,
|
||||
getReportProduct,
|
||||
getReportPayment,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { QuotationStatus } from 'src/stores/quotations/types';
|
|||
import { ProductGroup } from '../product-service/types';
|
||||
import { User } from '../user';
|
||||
import { CustomerBranch } from '../customer';
|
||||
import { PaymentDataStatus } from '../payment/types';
|
||||
|
||||
export type ReportQuotation = {
|
||||
updatedAt: Date | null;
|
||||
|
|
@ -34,6 +35,12 @@ export type ReportProduct = {
|
|||
code: string;
|
||||
};
|
||||
|
||||
export type ReportPayment = {
|
||||
year: string;
|
||||
month: string;
|
||||
data: Partial<Record<PaymentDataStatus, number>>;
|
||||
};
|
||||
|
||||
export type ReportSale = {
|
||||
byCustomer: (Omit<CustomerBranch, '_count'> & { _count: number })[];
|
||||
bySale: (User & { _count: number })[];
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ export type RequestData = {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
customerRequestCancel?: boolean;
|
||||
customerRequestCancelReason?: string;
|
||||
|
||||
quotation: QuotationFull & {
|
||||
debitNoteQuotationId: string;
|
||||
isDebitNote: boolean;
|
||||
|
|
@ -60,6 +63,8 @@ export type RequestWork = {
|
|||
attributes?: Attributes;
|
||||
creditNoteId?: string;
|
||||
processByUserId?: string;
|
||||
customerRequestCancel?: boolean;
|
||||
customerRequestCancelReason?: string;
|
||||
};
|
||||
|
||||
export type RowDocument = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue