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">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
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(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|
@ -38,7 +39,7 @@ withDefaults(
|
||||||
v-if="typeof value === 'string'"
|
v-if="typeof value === 'string'"
|
||||||
@click="$emit('labelClick', value, null)"
|
@click="$emit('labelClick', value, null)"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ formatNumberDecimal(+value, 2) }}
|
||||||
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
|
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span v-else :class="{ 'link cursor-pointer': clickable }">
|
<span v-else :class="{ 'link cursor-pointer': clickable }">
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ export default {
|
||||||
group: 'Group',
|
group: 'Group',
|
||||||
laborIdentified: 'Labor identified',
|
laborIdentified: 'Labor identified',
|
||||||
beDue: 'Due in',
|
beDue: 'Due in',
|
||||||
|
noReason: 'No reason',
|
||||||
due: 'Due',
|
due: 'Due',
|
||||||
overDue: 'Overdue',
|
overDue: 'Overdue',
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
|
|
@ -936,6 +937,7 @@ export default {
|
||||||
InProgress: 'In Progress',
|
InProgress: 'In Progress',
|
||||||
Completed: 'Completed',
|
Completed: 'Completed',
|
||||||
Canceled: 'Canceled',
|
Canceled: 'Canceled',
|
||||||
|
CancelRequested: 'Cancel Requested',
|
||||||
|
|
||||||
AwaitOrder: 'Awaiting Order',
|
AwaitOrder: 'Awaiting Order',
|
||||||
ReadyOrder: 'Ready for Order',
|
ReadyOrder: 'Ready for Order',
|
||||||
|
|
@ -1336,7 +1338,6 @@ export default {
|
||||||
Succeed: 'Completed',
|
Succeed: 'Completed',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
report: {
|
report: {
|
||||||
report: {
|
report: {
|
||||||
title: '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: 'กลุ่ม',
|
group: 'กลุ่ม',
|
||||||
laborIdentified: 'ระบุแรงงาน',
|
laborIdentified: 'ระบุแรงงาน',
|
||||||
beDue: 'จะครบกำหนดในอีก',
|
beDue: 'จะครบกำหนดในอีก',
|
||||||
|
noReason: 'ไม่มีเหตุผล',
|
||||||
due: 'ครบกำหนด',
|
due: 'ครบกำหนด',
|
||||||
overDue: 'เลยกำหนด',
|
overDue: 'เลยกำหนด',
|
||||||
status: 'สถานะ',
|
status: 'สถานะ',
|
||||||
|
|
@ -924,6 +925,7 @@ export default {
|
||||||
InProgress: 'กำลังดำเนินการ',
|
InProgress: 'กำลังดำเนินการ',
|
||||||
Completed: 'เสร็จสิ้น',
|
Completed: 'เสร็จสิ้น',
|
||||||
Canceled: 'ยกเลิก',
|
Canceled: 'ยกเลิก',
|
||||||
|
CancelRequested: 'ต้องการยกเลิก',
|
||||||
|
|
||||||
AwaitOrder: 'รอสั่งงาน',
|
AwaitOrder: 'รอสั่งงาน',
|
||||||
ReadyOrder: 'พร้อมสั่งงาน',
|
ReadyOrder: 'พร้อมสั่งงาน',
|
||||||
|
|
@ -1351,4 +1353,36 @@ export default {
|
||||||
count: 'จำนวนที่ขาย',
|
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">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
|
||||||
import { baseUrl } from 'src/stores/utils';
|
import { baseUrl } from 'src/stores/utils';
|
||||||
|
|
||||||
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
|
||||||
|
|
||||||
import { ProductRelation, PayCondition } from 'src/stores/quotations/types';
|
import { ProductRelation, PayCondition } from 'src/stores/quotations/types';
|
||||||
import { Step, RequestWorkStatus } from 'src/stores/request-list/types';
|
import { Step, RequestWorkStatus } from 'src/stores/request-list/types';
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
const workStatus = ref([
|
|
||||||
RequestWorkStatus.Ready,
|
|
||||||
RequestWorkStatus.Waiting,
|
|
||||||
RequestWorkStatus.InProgress,
|
|
||||||
RequestWorkStatus.Validate,
|
|
||||||
RequestWorkStatus.Ended,
|
|
||||||
RequestWorkStatus.Completed,
|
|
||||||
]);
|
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(
|
(
|
||||||
|
|
@ -29,6 +18,8 @@ const props = defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
status?: Step;
|
status?: Step;
|
||||||
imgUrl?: string;
|
imgUrl?: string;
|
||||||
|
requestCancel?: boolean;
|
||||||
|
requestCancelReason?: string;
|
||||||
installmentInfo?: {
|
installmentInfo?: {
|
||||||
total: number;
|
total: number;
|
||||||
paid?: number;
|
paid?: number;
|
||||||
|
|
@ -108,6 +99,18 @@ function changeableStatus(currentStatus?: RequestWorkStatus) {
|
||||||
<div class="rounded q-px-xs app-text-muted surface-3">
|
<div class="rounded q-px-xs app-text-muted surface-3">
|
||||||
{{ product?.code || code }}
|
{{ product?.code || code }}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="q-ml-auto q-gutter-y-xs">
|
<div class="q-ml-auto q-gutter-y-xs">
|
||||||
|
|
|
||||||
|
|
@ -731,6 +731,8 @@ async function submitRequestAction(data: {
|
||||||
<!-- product -->
|
<!-- product -->
|
||||||
<template v-for="(value, index) in productsList" :key="value">
|
<template v-for="(value, index) in productsList" :key="value">
|
||||||
<ProductExpansion
|
<ProductExpansion
|
||||||
|
:request-cancel="value.customerRequestCancel"
|
||||||
|
:request-cancel-reason="value.customerRequestCancelReason"
|
||||||
:cancel="data.requestDataStatus === RequestDataStatus.Canceled"
|
:cancel="data.requestDataStatus === RequestDataStatus.Canceled"
|
||||||
:readonly="
|
:readonly="
|
||||||
data.requestDataStatus === RequestDataStatus.Canceled ||
|
data.requestDataStatus === RequestDataStatus.Canceled ||
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,24 @@ function getEmployeeName(
|
||||||
$t(`requestList.status.${props.row.requestDataStatus}`) || '-'
|
$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>
|
||||||
<q-td class="text-right">
|
<q-td class="text-right">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,209 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// NOTE: Library
|
// NOTE: Library
|
||||||
|
import { onMounted, reactive } from 'vue';
|
||||||
|
|
||||||
// NOTE: Components
|
// 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
|
// 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
|
// 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>
|
</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>
|
<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 { defineStore } from 'pinia';
|
||||||
import { Pagination, Status } from '../types';
|
import { Pagination, Status } from '../types';
|
||||||
import { api } from 'src/boot/axios';
|
import { api } from 'src/boot/axios';
|
||||||
import { Report, ReportProduct, ReportQuotation, ReportSale } from './types';
|
import {
|
||||||
|
Report,
|
||||||
|
ReportPayment,
|
||||||
|
ReportProduct,
|
||||||
|
ReportQuotation,
|
||||||
|
ReportSale,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
const ENDPOINT = 'report';
|
const ENDPOINT = 'report';
|
||||||
|
|
||||||
|
|
@ -48,12 +54,21 @@ export async function getReportProduct() {
|
||||||
return null;
|
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', () => {
|
export const useReportStore = defineStore('report-store', () => {
|
||||||
const dataReportQuotation = ref<ReportQuotation[]>([]);
|
const dataReportQuotation = ref<ReportQuotation[]>([]);
|
||||||
const dataReportInvoice = ref<Report[]>([]);
|
const dataReportInvoice = ref<Report[]>([]);
|
||||||
const dataReportReceipt = ref<Report[]>([]);
|
const dataReportReceipt = ref<Report[]>([]);
|
||||||
const dataReportSale = ref<ReportSale>();
|
const dataReportSale = ref<ReportSale>();
|
||||||
const dataReportProduct = ref<ReportProduct[]>([]);
|
const dataReportProduct = ref<ReportProduct[]>([]);
|
||||||
|
const dataReportPayment = ref<ReportPayment[]>([]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataReportQuotation,
|
dataReportQuotation,
|
||||||
|
|
@ -61,11 +76,13 @@ export const useReportStore = defineStore('report-store', () => {
|
||||||
dataReportReceipt,
|
dataReportReceipt,
|
||||||
dataReportSale,
|
dataReportSale,
|
||||||
dataReportProduct,
|
dataReportProduct,
|
||||||
|
dataReportPayment,
|
||||||
|
|
||||||
getReportQuotation,
|
getReportQuotation,
|
||||||
getReportInvoice,
|
getReportInvoice,
|
||||||
getReportReceipt,
|
getReportReceipt,
|
||||||
getReportSale,
|
getReportSale,
|
||||||
getReportProduct,
|
getReportProduct,
|
||||||
|
getReportPayment,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { QuotationStatus } from 'src/stores/quotations/types';
|
||||||
import { ProductGroup } from '../product-service/types';
|
import { ProductGroup } from '../product-service/types';
|
||||||
import { User } from '../user';
|
import { User } from '../user';
|
||||||
import { CustomerBranch } from '../customer';
|
import { CustomerBranch } from '../customer';
|
||||||
|
import { PaymentDataStatus } from '../payment/types';
|
||||||
|
|
||||||
export type ReportQuotation = {
|
export type ReportQuotation = {
|
||||||
updatedAt: Date | null;
|
updatedAt: Date | null;
|
||||||
|
|
@ -34,6 +35,12 @@ export type ReportProduct = {
|
||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReportPayment = {
|
||||||
|
year: string;
|
||||||
|
month: string;
|
||||||
|
data: Partial<Record<PaymentDataStatus, number>>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ReportSale = {
|
export type ReportSale = {
|
||||||
byCustomer: (Omit<CustomerBranch, '_count'> & { _count: number })[];
|
byCustomer: (Omit<CustomerBranch, '_count'> & { _count: number })[];
|
||||||
bySale: (User & { _count: number })[];
|
bySale: (User & { _count: number })[];
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ export type RequestData = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
||||||
|
customerRequestCancel?: boolean;
|
||||||
|
customerRequestCancelReason?: string;
|
||||||
|
|
||||||
quotation: QuotationFull & {
|
quotation: QuotationFull & {
|
||||||
debitNoteQuotationId: string;
|
debitNoteQuotationId: string;
|
||||||
isDebitNote: boolean;
|
isDebitNote: boolean;
|
||||||
|
|
@ -60,6 +63,8 @@ export type RequestWork = {
|
||||||
attributes?: Attributes;
|
attributes?: Attributes;
|
||||||
creditNoteId?: string;
|
creditNoteId?: string;
|
||||||
processByUserId?: string;
|
processByUserId?: string;
|
||||||
|
customerRequestCancel?: boolean;
|
||||||
|
customerRequestCancelReason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RowDocument = {
|
export type RowDocument = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue