Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
This commit is contained in:
commit
2b35db5683
28 changed files with 1249 additions and 191 deletions
|
|
@ -47,7 +47,7 @@ const rand = Math.random();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="surface-1 rounded q-pa-sm quo-card bordered column"
|
class="surface-1 rounded q-pa-sm quo-card bordered column full-width"
|
||||||
:class="{ 'urgent-card': urgent }"
|
:class="{ 'urgent-card': urgent }"
|
||||||
:style="{ '--animation-delay': rand + 's' }"
|
:style="{ '--animation-delay': rand + 's' }"
|
||||||
>
|
>
|
||||||
|
|
@ -110,8 +110,8 @@ const rand = Math.random();
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="ellipsis q-px-xs">
|
<div class="text-bold q-px-xs ellipsis full-width">
|
||||||
<b>{{ title || '-' }}</b>
|
{{ title || '-' }}
|
||||||
<q-tooltip anchor="bottom start" self="top left">
|
<q-tooltip anchor="bottom start" self="top left">
|
||||||
{{ title || '-' }}
|
{{ title || '-' }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const props = withDefaults(
|
||||||
hideEdit?: boolean;
|
hideEdit?: boolean;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
hideBtnPreview?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
row: () => [],
|
row: () => [],
|
||||||
|
|
@ -127,6 +128,7 @@ defineEmits<{
|
||||||
|
|
||||||
<q-td class="text-right">
|
<q-td class="text-right">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="!hideBtnPreview"
|
||||||
:id="`btn-preview-${props.row.workName}`"
|
:id="`btn-preview-${props.row.workName}`"
|
||||||
icon="mdi-play-box-outline"
|
icon="mdi-play-box-outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ defineProps<{
|
||||||
:default-opened="defaultOpened"
|
:default-opened="defaultOpened"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>
|
<slot name="header" />
|
||||||
<slot name="header" />
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<main class="surface-1 q-pa-md full-width">
|
<main class="surface-1 q-pa-md full-width">
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ div.fullscreen.q-drawer__backdrop {
|
||||||
font-family: 'Noto Sans Thai', sans-serif;
|
font-family: 'Noto Sans Thai', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dp__overlay_cell_active.dp__overlay_cell_pad {
|
||||||
|
font-family: 'Noto Sans Thai', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
.disabled,
|
.disabled,
|
||||||
.disabled *,
|
.disabled *,
|
||||||
[disabled],
|
[disabled],
|
||||||
|
|
|
||||||
105
src/i18n/eng.ts
105
src/i18n/eng.ts
|
|
@ -149,6 +149,7 @@ export default {
|
||||||
included: 'Included',
|
included: 'Included',
|
||||||
notIncluded: 'Not Included',
|
notIncluded: 'Not Included',
|
||||||
dueDate: 'Due date',
|
dueDate: 'Due date',
|
||||||
|
year: 'year',
|
||||||
},
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
|
|
@ -241,6 +242,16 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noti: {
|
||||||
|
title: 'Notification',
|
||||||
|
caption: 'All Notification',
|
||||||
|
unread: 'Unread',
|
||||||
|
all: 'All',
|
||||||
|
read: 'Read',
|
||||||
|
viewALL: 'View All',
|
||||||
|
markAsRead: 'Mark as Read',
|
||||||
|
},
|
||||||
|
|
||||||
form: {
|
form: {
|
||||||
tm6: {
|
tm6: {
|
||||||
transportation: 'Flight/Vehicle',
|
transportation: 'Flight/Vehicle',
|
||||||
|
|
@ -791,7 +802,7 @@ export default {
|
||||||
|
|
||||||
paySplitMessage: 'Amount to be paid (Baht)',
|
paySplitMessage: 'Amount to be paid (Baht)',
|
||||||
summary: 'Total Summary',
|
summary: 'Total Summary',
|
||||||
periodNo: 'Installment No."',
|
periodNo: 'Installment No.',
|
||||||
amount: 'Amount',
|
amount: 'Amount',
|
||||||
payDueDate: 'Pay Due Date',
|
payDueDate: 'Pay Due Date',
|
||||||
callDueDate: 'Call Due Date',
|
callDueDate: 'Call Due Date',
|
||||||
|
|
@ -1141,6 +1152,7 @@ export default {
|
||||||
taskListNotFound: 'Task list cannot be found.',
|
taskListNotFound: 'Task list cannot be found.',
|
||||||
creditNoteNotFound: 'Credit note cannot be found.',
|
creditNoteNotFound: 'Credit note cannot be found.',
|
||||||
debitNoteNotFound: 'Debit note cannot be found.',
|
debitNoteNotFound: 'Debit note cannot be found.',
|
||||||
|
notificationNotFound: 'Notification cannot be found.',
|
||||||
|
|
||||||
productGroupIsUsed: 'Product group is in used.',
|
productGroupIsUsed: 'Product group is in used.',
|
||||||
productIsUsed: 'Product is in used.',
|
productIsUsed: 'Product is in used.',
|
||||||
|
|
@ -1339,39 +1351,49 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
report: {
|
report: {
|
||||||
report: {
|
title: 'Report',
|
||||||
title: 'Report',
|
value: 'Value',
|
||||||
view: {
|
customerName: 'Customer Name',
|
||||||
Document: 'Document Status Report',
|
view: {
|
||||||
Invoice: 'Payment Report',
|
Document: 'Document Status Report',
|
||||||
Product: 'Product and Service Report',
|
Invoice: 'Payment Report',
|
||||||
Sale: 'Sales Summary Report',
|
Product: 'Product and Service Report',
|
||||||
},
|
Sale: 'Sales Summary Report',
|
||||||
document: {
|
Profit: 'Profit and Loss Report',
|
||||||
code: 'Code',
|
},
|
||||||
status: 'Quotation Status',
|
document: {
|
||||||
createAt: 'Created Date',
|
code: 'Code',
|
||||||
updateAt: 'Updated Date',
|
status: 'Quotation Status',
|
||||||
},
|
createAt: 'Created Date',
|
||||||
table: {
|
updateAt: 'Updated Date',
|
||||||
code: 'Code',
|
},
|
||||||
status: 'Status',
|
table: {
|
||||||
createAt: 'Created Date',
|
code: 'Code',
|
||||||
},
|
status: 'Status',
|
||||||
product: {
|
createAt: 'Created Date',
|
||||||
did: 'Processed Quantity',
|
},
|
||||||
sale: 'Sold Quantity',
|
product: {
|
||||||
name: 'Product Name',
|
did: 'Processed Quantity',
|
||||||
code: 'Product Code',
|
sale: 'Sold Quantity',
|
||||||
},
|
name: 'Product Name',
|
||||||
sale: {
|
code: 'Product Code',
|
||||||
byCustomer: 'Sales by Branch',
|
},
|
||||||
byProductGroup: 'Sales by Product Category',
|
sale: {
|
||||||
bySale: 'Sales by Salesperson',
|
byCustomer: 'Sales by Branch',
|
||||||
code: 'Code',
|
byProductGroup: 'Sales by Product Category',
|
||||||
name: 'Name',
|
bySale: 'Sales by Salesperson',
|
||||||
count: 'Sold Quantity',
|
code: 'Code',
|
||||||
},
|
name: 'Name',
|
||||||
|
count: 'Sold Quantity',
|
||||||
|
},
|
||||||
|
profit: {
|
||||||
|
byMonth: 'Profit Table by Month',
|
||||||
|
byYear: 'Profit Table by Year',
|
||||||
|
month: 'Month',
|
||||||
|
year: 'Year (AD)',
|
||||||
|
netProfit: 'Net Profit',
|
||||||
|
expenses: 'Expenses',
|
||||||
|
income: 'Income',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
|
|
@ -1406,4 +1428,19 @@ export default {
|
||||||
caption: 'Based on Tax Invoices',
|
caption: 'Based on Tax Invoices',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
month: {
|
||||||
|
1: 'January',
|
||||||
|
2: 'February',
|
||||||
|
3: 'March',
|
||||||
|
4: 'April',
|
||||||
|
5: 'May',
|
||||||
|
6: 'June',
|
||||||
|
7: 'July',
|
||||||
|
8: 'August',
|
||||||
|
9: 'September',
|
||||||
|
10: 'October',
|
||||||
|
11: 'November',
|
||||||
|
12: 'December',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ export default {
|
||||||
included: 'รวม',
|
included: 'รวม',
|
||||||
notIncluded: 'ไม่รวม',
|
notIncluded: 'ไม่รวม',
|
||||||
dueDate: 'วันครบกำหนด',
|
dueDate: 'วันครบกำหนด',
|
||||||
|
year: 'ปี',
|
||||||
},
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
|
|
@ -241,6 +242,16 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noti: {
|
||||||
|
title: 'การแจ้งเตือน',
|
||||||
|
caption: 'การแจ้งเตือนทั้งหมด',
|
||||||
|
unread: 'ยังไม่ได้อ่าน',
|
||||||
|
all: 'ทั้งหมด',
|
||||||
|
read: 'อ่าน',
|
||||||
|
viewAll: 'ดูทั้งหมด',
|
||||||
|
markAsRead: 'ทำเครื่องหมายว่าอ่านแล้ว',
|
||||||
|
},
|
||||||
|
|
||||||
form: {
|
form: {
|
||||||
tm6: {
|
tm6: {
|
||||||
transportation: 'เที่ยวบิน/พาหนะ',
|
transportation: 'เที่ยวบิน/พาหนะ',
|
||||||
|
|
@ -1119,6 +1130,7 @@ export default {
|
||||||
taskListNotFound: 'ไม่พบใบสั่งงาน',
|
taskListNotFound: 'ไม่พบใบสั่งงาน',
|
||||||
creditNoteNotFound: 'ไม่พบใบลดหนี้',
|
creditNoteNotFound: 'ไม่พบใบลดหนี้',
|
||||||
debitNoteNotFound: 'ไม่พบใบเพิ่มหนี้',
|
debitNoteNotFound: 'ไม่พบใบเพิ่มหนี้',
|
||||||
|
notificationNotFound: 'ไม่พบการแจ้งเตือน',
|
||||||
|
|
||||||
productGroupIsUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
|
productGroupIsUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
|
||||||
productIsUsed: 'สินค้าและบริการใช้งานอยู่',
|
productIsUsed: 'สินค้าและบริการใช้งานอยู่',
|
||||||
|
|
@ -1320,11 +1332,14 @@ export default {
|
||||||
|
|
||||||
report: {
|
report: {
|
||||||
title: 'รายงาน',
|
title: 'รายงาน',
|
||||||
|
customerName: 'ชื่อลูกค้า',
|
||||||
|
value: 'มูลค่า',
|
||||||
view: {
|
view: {
|
||||||
Document: 'รายงานสถานะเอกสาร',
|
Document: 'รายงานสถานะเอกสาร',
|
||||||
Invoice: 'รายงานการชำระเงิน',
|
Invoice: 'รายงานการชำระเงิน',
|
||||||
Product: 'รายงานสินค้าและบริการ',
|
Product: 'รายงานสินค้าและบริการ',
|
||||||
Sale: 'รายงานสรุปยอดขาย',
|
Sale: 'รายงานสรุปยอดขาย',
|
||||||
|
Profit: 'รายงานกำไรและขาดทุน',
|
||||||
},
|
},
|
||||||
document: {
|
document: {
|
||||||
code: 'รหัส',
|
code: 'รหัส',
|
||||||
|
|
@ -1352,6 +1367,15 @@ export default {
|
||||||
name: 'ชื่อ',
|
name: 'ชื่อ',
|
||||||
count: 'จำนวนที่ขาย',
|
count: 'จำนวนที่ขาย',
|
||||||
},
|
},
|
||||||
|
profit: {
|
||||||
|
byMonth: 'ตารางผลกำไรตามเดือน',
|
||||||
|
byYear: 'ตารางผลกำไรตามปี',
|
||||||
|
month: 'เดือน',
|
||||||
|
year: 'ปี (ค.ศ.)',
|
||||||
|
netProfit: 'กำไร',
|
||||||
|
expenses: 'ต้นทุน',
|
||||||
|
income: 'รายได้',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
|
|
@ -1385,4 +1409,19 @@ export default {
|
||||||
caption: 'ตามใบกำกับภาษี',
|
caption: 'ตามใบกำกับภาษี',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
month: {
|
||||||
|
1: 'มกราคม',
|
||||||
|
2: 'กุมภาพันธ์',
|
||||||
|
3: 'มีนาคม',
|
||||||
|
4: 'เมษายน',
|
||||||
|
5: 'พฤษภาคม',
|
||||||
|
6: 'มิถุนายน',
|
||||||
|
7: 'กรกฎาคม',
|
||||||
|
8: 'สิงหาคม',
|
||||||
|
9: 'กันยายน',
|
||||||
|
10: 'ตุลาคม',
|
||||||
|
11: 'พฤศจิกายน',
|
||||||
|
12: 'ธันวาคม',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed, reactive } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
|
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import useLoader from 'stores/loader';
|
import useLoader from 'stores/loader';
|
||||||
import ProfileMenu from './ProfileMenu.vue';
|
import ProfileMenu from './ProfileMenu.vue';
|
||||||
|
|
@ -17,6 +18,8 @@ import { useConfigStore } from 'src/stores/config';
|
||||||
import { useNavigator } from 'src/stores/navigator';
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
import { initLang, initTheme, Lang, setLang } from 'src/utils/ui';
|
import { initLang, initTheme, Lang, setLang } from 'src/utils/ui';
|
||||||
import { baseUrl } from 'stores/utils';
|
import { baseUrl } from 'stores/utils';
|
||||||
|
import { useNotification } from 'src/stores/notification';
|
||||||
|
import NotiDialog from 'src/pages/00_notification/NotiDialog.vue';
|
||||||
|
|
||||||
const useMyBranch = useMyBranchStore();
|
const useMyBranch = useMyBranchStore();
|
||||||
const { fetchListMyBranch } = useMyBranch;
|
const { fetchListMyBranch } = useMyBranch;
|
||||||
|
|
@ -27,18 +30,14 @@ interface NotificationButton {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
read: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const loaderStore = useLoader();
|
const loaderStore = useLoader();
|
||||||
const navigatorStore = useNavigator();
|
const navigatorStore = useNavigator();
|
||||||
|
const notificationStore = useNotification();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
const { data: notificationData } = storeToRefs(notificationStore);
|
||||||
|
|
||||||
const { visible } = storeToRefs(loaderStore);
|
const { visible } = storeToRefs(loaderStore);
|
||||||
const { t } = useI18n({ useScope: 'global' });
|
const { t } = useI18n({ useScope: 'global' });
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
@ -48,8 +47,9 @@ const canvasModal = ref(false);
|
||||||
const leftDrawerOpen = ref<boolean>(false);
|
const leftDrawerOpen = ref<boolean>(false);
|
||||||
const leftDrawerMini = ref(false);
|
const leftDrawerMini = ref(false);
|
||||||
|
|
||||||
const filterUnread = ref(false);
|
const unread = computed<number>(
|
||||||
const unread = ref<number>(1);
|
() => notificationData.value.filter((v) => !v.read).length || 0,
|
||||||
|
);
|
||||||
// const filterRole = ref<string[]>();
|
// const filterRole = ref<string[]>();
|
||||||
const userImage = ref<string>();
|
const userImage = ref<string>();
|
||||||
const userGender = ref('');
|
const userGender = ref('');
|
||||||
|
|
@ -65,33 +65,24 @@ const language: {
|
||||||
{ value: Lang.English, label: 'English', icon: 'us', date: 'en-gb' },
|
{ value: Lang.English, label: 'English', icon: 'us', date: 'en-gb' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const notiOpen = ref(false);
|
const state = reactive({
|
||||||
|
filterUnread: false,
|
||||||
|
notiOpen: false,
|
||||||
|
notiDialog: false,
|
||||||
|
notiId: '',
|
||||||
|
});
|
||||||
const notiMenu = ref<NotificationButton[]>([
|
const notiMenu = ref<NotificationButton[]>([
|
||||||
{
|
{
|
||||||
item: 'ทั้งหมด',
|
item: 'all',
|
||||||
color: 'noti-switch-on',
|
color: 'noti-switch-on',
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
item: 'ยังไม่ได้อ่าน',
|
item: 'unread',
|
||||||
color: 'noti-switch-off',
|
color: 'noti-switch-off',
|
||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const notification = ref<Notification[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Unread',
|
|
||||||
content: 'Unread',
|
|
||||||
read: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'Read',
|
|
||||||
content: 'Already read',
|
|
||||||
read: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
function setActive(button: NotificationButton) {
|
function setActive(button: NotificationButton) {
|
||||||
notiMenu.value = notiMenu.value.map((current) => ({
|
notiMenu.value = notiMenu.value.map((current) => ({
|
||||||
|
|
@ -99,12 +90,12 @@ function setActive(button: NotificationButton) {
|
||||||
color: current.item !== button.item ? 'noti-switch-off' : 'noti-switch-on',
|
color: current.item !== button.item ? 'noti-switch-off' : 'noti-switch-on',
|
||||||
active: current.item === button.item,
|
active: current.item === button.item,
|
||||||
}));
|
}));
|
||||||
if (button.item === 'ยังไม่ได้อ่าน') {
|
if (button.item === 'unread') {
|
||||||
// noti.value?.result &&
|
// noti.value?.result &&
|
||||||
filterUnread.value = true;
|
state.filterUnread = true;
|
||||||
}
|
}
|
||||||
if (button.item === 'ทั้งหมด') {
|
if (button.item === 'all') {
|
||||||
filterUnread.value = false;
|
state.filterUnread = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,12 +114,25 @@ function doLogout() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readNoti(id: string) {
|
||||||
|
state.notiDialog = true;
|
||||||
|
state.notiId = id;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initTheme();
|
initTheme();
|
||||||
initLang();
|
initLang();
|
||||||
|
|
||||||
await configStore.getConfig();
|
await configStore.getConfig();
|
||||||
|
|
||||||
|
{
|
||||||
|
const noti = await notificationStore.getNotificationList();
|
||||||
|
|
||||||
|
if (noti) {
|
||||||
|
notificationData.value = noti.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fetchListMyBranch(getUserId() ?? '');
|
await fetchListMyBranch(getUserId() ?? '');
|
||||||
leftDrawerOpen.value = $q.screen.gt.xs ? true : false;
|
leftDrawerOpen.value = $q.screen.gt.xs ? true : false;
|
||||||
|
|
||||||
|
|
@ -278,7 +282,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
<div class="row q-gutter-x-md items-center" style="margin-left: auto">
|
<div class="row q-gutter-x-md items-center" style="margin-left: auto">
|
||||||
<!-- notification -->
|
<!-- notification -->
|
||||||
<!-- <q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
|
|
@ -286,7 +290,7 @@ onMounted(async () => {
|
||||||
:size="$q.screen.lt.sm ? 'sm' : ''"
|
:size="$q.screen.lt.sm ? 'sm' : ''"
|
||||||
:class="{ bordered: $q.dark.isActive, dark: $q.dark.isActive }"
|
:class="{ bordered: $q.dark.isActive, dark: $q.dark.isActive }"
|
||||||
style="color: var(--surface-1)"
|
style="color: var(--surface-1)"
|
||||||
@click="notiOpen = !notiOpen"
|
@click="state.notiOpen = !state.notiOpen"
|
||||||
>
|
>
|
||||||
<q-icon name="mdi-bell" />
|
<q-icon name="mdi-bell" />
|
||||||
<q-badge v-if="unread !== 0" rounded floating color="negative">
|
<q-badge v-if="unread !== 0" rounded floating color="negative">
|
||||||
|
|
@ -297,10 +301,16 @@ onMounted(async () => {
|
||||||
:offset="[0, 10]"
|
:offset="[0, 10]"
|
||||||
anchor="bottom middle"
|
anchor="bottom middle"
|
||||||
self="top middle"
|
self="top middle"
|
||||||
@before-hide="notiOpen = false"
|
@before-hide="
|
||||||
|
() => {
|
||||||
|
state.notiOpen = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="q-px-md q-py-sm row col-12 items-center">
|
<div class="q-px-md q-py-sm row col-12 items-center">
|
||||||
<div class="text-subtitle1 text-weight-bold">แจ้งเตือน</div>
|
<div class="text-subtitle1 text-weight-bold">
|
||||||
|
{{ $t('noti.title') }}
|
||||||
|
</div>
|
||||||
<q-space />
|
<q-space />
|
||||||
</div>
|
</div>
|
||||||
<div class="q-px-md q-pb-md q-gutter-x-md">
|
<div class="q-px-md q-pb-md q-gutter-x-md">
|
||||||
|
|
@ -312,22 +322,30 @@ onMounted(async () => {
|
||||||
:flat="!btn.active"
|
:flat="!btn.active"
|
||||||
:unelevated="btn.active"
|
:unelevated="btn.active"
|
||||||
:key="index"
|
:key="index"
|
||||||
:label="btn.item"
|
:label="$t('noti.' + btn.item)"
|
||||||
:class="btn.color"
|
:class="btn.color"
|
||||||
@click="setActive(btn)"
|
@click="setActive(btn)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-infinite-scroll :offset="250">
|
<div style="max-height: 30vh; width: 400px; overflow-y: auto">
|
||||||
<div class="caption cursor-pointer">
|
<section
|
||||||
|
v-if="
|
||||||
|
state.filterUnread
|
||||||
|
? notificationData.filter((v) => !v.read).length
|
||||||
|
: notificationData.length
|
||||||
|
"
|
||||||
|
class="caption cursor-pointer"
|
||||||
|
>
|
||||||
<q-item
|
<q-item
|
||||||
|
v-for="(item, i) in state.filterUnread
|
||||||
|
? notificationData.filter((v) => !v.read)
|
||||||
|
: notificationData"
|
||||||
dense
|
dense
|
||||||
clickable
|
clickable
|
||||||
class="q-py-sm"
|
class="q-py-sm"
|
||||||
v-ripple
|
v-ripple
|
||||||
v-for="item in !filterUnread
|
@click="readNoti(item.id)"
|
||||||
? notification
|
:key="i"
|
||||||
: notification.filter((v) => !v.read)"
|
|
||||||
:key="item.id"
|
|
||||||
>
|
>
|
||||||
<q-avatar
|
<q-avatar
|
||||||
color="positive"
|
color="positive"
|
||||||
|
|
@ -343,12 +361,11 @@ onMounted(async () => {
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</span>
|
</span>
|
||||||
<span class="block ellipsis full-width text-stone">
|
<span class="block ellipsis full-width text-stone">
|
||||||
{{ item.content }}
|
{{ item.detail }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span align="right" class="col text-caption text-stone">
|
<span align="right" class="col text-caption text-stone">
|
||||||
{{ moment(item.createdAt).fromNow() }}
|
{{ moment(item.createdAt).fromNow() }}
|
||||||
5 s
|
|
||||||
</span>
|
</span>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
anchor="top middle"
|
anchor="top middle"
|
||||||
|
|
@ -356,21 +373,29 @@ onMounted(async () => {
|
||||||
:delay="1000"
|
:delay="1000"
|
||||||
:offset="[10, 10]"
|
:offset="[10, 10]"
|
||||||
>
|
>
|
||||||
{{ item.content }}
|
{{ item.detail }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</section>
|
||||||
<template v-slot:loading>
|
<section v-else class="text-center q-py-sm">
|
||||||
<div
|
<span class="app-text-muted">
|
||||||
class="text-center q-my-md"
|
{{ $t('general.noData') }}
|
||||||
v-if="noti && noti?.result.length < noti?.total"
|
</span>
|
||||||
>
|
</section>
|
||||||
<q-spinner-dots color="primary" size="40px" />
|
</div>
|
||||||
</div>
|
<div class="col bordered-t">
|
||||||
</template>
|
<q-btn
|
||||||
</q-infinite-scroll>
|
flat
|
||||||
|
dense
|
||||||
|
color="info"
|
||||||
|
class="full-width"
|
||||||
|
@click="() => $router.push('/notification')"
|
||||||
|
>
|
||||||
|
{{ $t('noti.viewAll') }}
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn> -->
|
</q-btn>
|
||||||
|
|
||||||
<!-- เปลี่นนภาษา -->
|
<!-- เปลี่นนภาษา -->
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -468,6 +493,8 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
</DialogForm>
|
</DialogForm>
|
||||||
|
|
||||||
|
<NotiDialog v-model="state.notiDialog" v-model:id="state.notiId" />
|
||||||
|
|
||||||
<global-loading :visibility="visible" />
|
<global-loading :visibility="visible" />
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
243
src/pages/00_notification/MainPage.vue
Normal file
243
src/pages/00_notification/MainPage.vue
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// NOTE: Library
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
// NOTE: Components
|
||||||
|
import NoData from 'src/components/NoData.vue';
|
||||||
|
import NotiDialog from './NotiDialog.vue';
|
||||||
|
|
||||||
|
// NOTE: Stores & Type
|
||||||
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useNotification } from 'src/stores/notification';
|
||||||
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
|
||||||
|
// NOTE: Variable
|
||||||
|
const navigatorStore = useNavigator();
|
||||||
|
const notificationStore = useNotification();
|
||||||
|
const { data: noti } = storeToRefs(notificationStore);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
currentTab: 'all',
|
||||||
|
inputSearch: '',
|
||||||
|
notiDialog: false,
|
||||||
|
notiId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageTabs = [
|
||||||
|
{ label: 'all', value: 'all' },
|
||||||
|
{ label: 'unread', value: 'unread' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedNoti = ref<string[]>([]);
|
||||||
|
|
||||||
|
function toggleSelection(id: string, checkAll: boolean = false) {
|
||||||
|
const index = selectedNoti.value.indexOf(id);
|
||||||
|
if (checkAll) {
|
||||||
|
if (selectedNoti.value.length === noti.value.length) {
|
||||||
|
selectedNoti.value = [];
|
||||||
|
} else {
|
||||||
|
selectedNoti.value = [...noti.value.map((v) => v.id)];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
selectedNoti.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
selectedNoti.value.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchNoti() {
|
||||||
|
const res = await notificationStore.getNotificationList();
|
||||||
|
if (res) {
|
||||||
|
noti.value = res.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markAsRead() {
|
||||||
|
const res = await notificationStore.markReadNotification(selectedNoti.value);
|
||||||
|
if (res) {
|
||||||
|
await fetchNoti();
|
||||||
|
selectedNoti.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteNoti() {
|
||||||
|
const res = await notificationStore.deleteMultiNotification(
|
||||||
|
selectedNoti.value,
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
await fetchNoti();
|
||||||
|
selectedNoti.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNoti(id: string) {
|
||||||
|
state.notiId = id;
|
||||||
|
state.notiDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navigatorStore.current.title = 'noti.title';
|
||||||
|
navigatorStore.current.path = [{ text: 'noti.caption', i18n: true }];
|
||||||
|
|
||||||
|
await fetchNoti();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<main class="column full-height no-wrap">
|
||||||
|
<div class="surface-1 col bordered rounded column">
|
||||||
|
<div class="q-px-lg q-py-xs row items-center">
|
||||||
|
<q-checkbox
|
||||||
|
size="xs"
|
||||||
|
:model-value="selectedNoti.length === noti.length"
|
||||||
|
class="q-px-sm"
|
||||||
|
@click="toggleSelection('', true)"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="selectedNoti.length === 0"
|
||||||
|
icon="mdi-refresh"
|
||||||
|
rounded
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
class="app-text-muted-2 q-ml-sm q-mt-xs"
|
||||||
|
@click="async () => await fetchNoti()"
|
||||||
|
>
|
||||||
|
<q-tooltip>Refresh</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="selectedNoti.length > 0"
|
||||||
|
icon="mdi-trash-can-outline"
|
||||||
|
rounded
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
class="app-text-muted-2 q-ml-sm"
|
||||||
|
@click="async () => await deleteNoti()"
|
||||||
|
>
|
||||||
|
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="selectedNoti.length > 0"
|
||||||
|
icon="mdi-email-open-outline"
|
||||||
|
rounded
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
class="app-text-muted-2 q-mx-sm"
|
||||||
|
@click="async () => await markAsRead()"
|
||||||
|
>
|
||||||
|
<q-tooltip>{{ $t('noti.markAsRead') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<span v-if="selectedNoti.length" class="text-caption app-text-muted-2">
|
||||||
|
{{
|
||||||
|
$t('general.selected', {
|
||||||
|
number: selectedNoti.length,
|
||||||
|
msg: $t('noti.title'),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-tabs
|
||||||
|
inline-label
|
||||||
|
mobile-arrows
|
||||||
|
dense
|
||||||
|
v-model="state.currentTab"
|
||||||
|
align="left"
|
||||||
|
class="full-width bordered-b"
|
||||||
|
active-color="info"
|
||||||
|
>
|
||||||
|
<q-tab
|
||||||
|
v-for="tab in pageTabs"
|
||||||
|
:name="tab.value"
|
||||||
|
:key="tab.value"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
state.currentTab = tab.value;
|
||||||
|
state.inputSearch = '';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row text-capitalize"
|
||||||
|
:class="
|
||||||
|
state.currentTab === tab.value ? 'text-bold' : 'app-text-muted'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t(`noti.${tab.label}`) }}
|
||||||
|
<q-badge
|
||||||
|
rounded
|
||||||
|
class="q-ml-md"
|
||||||
|
style="background: hsl(var(--info-bg))"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
tab.value === 'all'
|
||||||
|
? noti.length
|
||||||
|
: noti.filter((v) => !v.read).length
|
||||||
|
}}
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
|
||||||
|
<section class="scroll full-height col">
|
||||||
|
<article
|
||||||
|
v-if="
|
||||||
|
state.currentTab === 'unread'
|
||||||
|
? noti.filter((v) => !v.read).length > 0
|
||||||
|
: noti.length > 0
|
||||||
|
"
|
||||||
|
class="q-pa-md q-gutter-sm"
|
||||||
|
>
|
||||||
|
<q-item
|
||||||
|
v-for="(n, i) in state.currentTab === 'unread'
|
||||||
|
? noti.filter((v) => !v.read)
|
||||||
|
: noti"
|
||||||
|
:key="i"
|
||||||
|
clickable
|
||||||
|
:class="{ 'surface-3': !n.read }"
|
||||||
|
class="q-py-sm q-px-md rounded row no-wrap items-center"
|
||||||
|
@click.stop="readNoti(n.id)"
|
||||||
|
>
|
||||||
|
<!-- <div
|
||||||
|
class="rounded"
|
||||||
|
:style="`background: hsl(var(--info-bg)/${n.read ? 0 : 1}); width: 6px; height: 6px`"
|
||||||
|
/> -->
|
||||||
|
<q-checkbox
|
||||||
|
:model-value="selectedNoti.includes(n.id)"
|
||||||
|
size="xs"
|
||||||
|
class="q-pr-lg"
|
||||||
|
@click.stop="toggleSelection(n.id)"
|
||||||
|
/>
|
||||||
|
<div class="column">
|
||||||
|
{{ n.title }}
|
||||||
|
<span class="text-caption app-text-muted">
|
||||||
|
{{ n.detail }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="q-ml-auto">
|
||||||
|
{{
|
||||||
|
dateFormatJS({
|
||||||
|
date: n.createdAt,
|
||||||
|
dayStyle: 'numeric',
|
||||||
|
monthStyle: '2-digit',
|
||||||
|
withTime: true,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</q-item>
|
||||||
|
</article>
|
||||||
|
<div v-else class="full-height flex items-center justify-center">
|
||||||
|
<NoData />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<NotiDialog v-model="state.notiDialog" v-model:id="state.notiId" />
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
69
src/pages/00_notification/NotiDialog.vue
Normal file
69
src/pages/00_notification/NotiDialog.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
|
||||||
|
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
|
||||||
|
import { CancelButton } from 'src/components/button';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Notification, useNotification } from 'src/stores/notification';
|
||||||
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
|
||||||
|
const notificationStore = useNotification();
|
||||||
|
const open = defineModel<boolean>({ default: false, required: true });
|
||||||
|
const notiId = defineModel<string>('id', { default: '', required: true });
|
||||||
|
|
||||||
|
const noti = ref<Notification>();
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchNoti() {
|
||||||
|
const res = await notificationStore.getNotification(notiId.value);
|
||||||
|
if (res) {
|
||||||
|
noti.value = res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<DialogFormContainer
|
||||||
|
width="60vw"
|
||||||
|
height="350px"
|
||||||
|
v-model="open"
|
||||||
|
v-on:open="fetchNoti"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<DialogHeader :title="$t('noti.title')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<section v-if="noti" class="q-pa-md col full-width">
|
||||||
|
<article
|
||||||
|
class="surface-1 rounded bordered q-pa-md full-height full-width"
|
||||||
|
>
|
||||||
|
<div class="text-bold">
|
||||||
|
{{ noti.title }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption app-text-muted">
|
||||||
|
{{
|
||||||
|
dateFormatJS({
|
||||||
|
date: noti.createdAt,
|
||||||
|
monthStyle: 'long',
|
||||||
|
withTime: true,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<q-separator spaced="md" />
|
||||||
|
<div class="text-caption">
|
||||||
|
{{ noti.detail }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<CancelButton
|
||||||
|
class="q-ml-auto"
|
||||||
|
:label="$t('general.close')"
|
||||||
|
outlined
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DialogFormContainer>
|
||||||
|
</template>
|
||||||
|
|
@ -641,6 +641,7 @@ async function storeDataLocal(id: string) {
|
||||||
:visible-columns="pageState.fieldSelected"
|
:visible-columns="pageState.fieldSelected"
|
||||||
:grid="pageState.gridView"
|
:grid="pageState.gridView"
|
||||||
:hide-edit="pageState.currentTab !== 'Issued'"
|
:hide-edit="pageState.currentTab !== 'Issued'"
|
||||||
|
:hide-btn-preview="pageState.currentTab === 'PaymentSuccess'"
|
||||||
@preview="(id: any) => storeDataLocal(id)"
|
@preview="(id: any) => storeDataLocal(id)"
|
||||||
@view="
|
@view="
|
||||||
(item) => {
|
(item) => {
|
||||||
|
|
@ -667,6 +668,7 @@ async function storeDataLocal(id: string) {
|
||||||
class="col"
|
class="col"
|
||||||
hide-kebab-delete
|
hide-kebab-delete
|
||||||
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
|
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
|
||||||
|
:hide-preview="pageState.currentTab === 'PaymentSuccess'"
|
||||||
:urgent="item.row.urgent"
|
:urgent="item.row.urgent"
|
||||||
:code="item.row.code"
|
:code="item.row.code"
|
||||||
:title="item.row.workName"
|
:title="item.row.workName"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ const prop = defineProps<{
|
||||||
isDebitNote?: boolean;
|
isDebitNote?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const firstCodePayment = defineModel<string>('firstCodePayment');
|
||||||
|
|
||||||
const refQFile = ref<InstanceType<typeof QFile>[]>([]);
|
const refQFile = ref<InstanceType<typeof QFile>[]>([]);
|
||||||
const refQMenu = ref<InstanceType<typeof QMenu>[]>([]);
|
const refQMenu = ref<InstanceType<typeof QMenu>[]>([]);
|
||||||
const paymentData = ref<QuotationPaymentData[]>([]);
|
const paymentData = ref<QuotationPaymentData[]>([]);
|
||||||
|
|
@ -206,10 +208,15 @@ onMounted(async () => {
|
||||||
});
|
});
|
||||||
if (ret) {
|
if (ret) {
|
||||||
paymentData.value = ret.result;
|
paymentData.value = ret.result;
|
||||||
slipFile.value = paymentData.value.map((v) => ({
|
slipFile.value = paymentData.value.map((v, i) => {
|
||||||
paymentId: v.id,
|
if (i === 0) {
|
||||||
data: [],
|
firstCodePayment.value = v.code;
|
||||||
}));
|
}
|
||||||
|
return {
|
||||||
|
paymentId: v.id,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ const selectedWorkerItem = computed(() => {
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
const workerList = ref<Employee[]>([]);
|
const workerList = ref<Employee[]>([]);
|
||||||
|
const firstCodePayment = ref('');
|
||||||
const selectedProductGroup = ref('');
|
const selectedProductGroup = ref('');
|
||||||
const selectedInstallmentNo = ref<number[]>([]);
|
const selectedInstallmentNo = ref<number[]>([]);
|
||||||
const installmentAmount = ref<number>(0);
|
const installmentAmount = ref<number>(0);
|
||||||
|
|
@ -1093,12 +1093,14 @@ watch(
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function storeDataLocal() {
|
function storeDataLocal() {
|
||||||
quotationFormData.value.productServiceList = productServiceList.value;
|
quotationFormData.value.productServiceList = productService.value;
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'quotation-preview',
|
'quotation-preview',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
data: {
|
data: {
|
||||||
|
codeInvoice: code.value,
|
||||||
|
codePayment: firstCodePayment.value,
|
||||||
...quotationFormData.value,
|
...quotationFormData.value,
|
||||||
productServiceList: productService.value,
|
productServiceList: productService.value,
|
||||||
},
|
},
|
||||||
|
|
@ -1939,6 +1941,7 @@ function covertToNode() {
|
||||||
view !== View.Complete
|
view !== View.Complete
|
||||||
"
|
"
|
||||||
:data="quotationFormState.source"
|
:data="quotationFormState.source"
|
||||||
|
v-model:first-code-payment="firstCodePayment"
|
||||||
@fetch-status="
|
@fetch-status="
|
||||||
() => {
|
() => {
|
||||||
fetchQuotation();
|
fetchQuotation();
|
||||||
|
|
@ -2105,6 +2108,8 @@ function covertToNode() {
|
||||||
|
|
||||||
installmentAmount = props.row.amount;
|
installmentAmount = props.row.amount;
|
||||||
view = View.Invoice;
|
view = View.Invoice;
|
||||||
|
|
||||||
|
console.log(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
|
@ -2289,6 +2294,7 @@ function covertToNode() {
|
||||||
<MainButton
|
<MainButton
|
||||||
solid
|
solid
|
||||||
icon="mdi-account-multiple-check-outline"
|
icon="mdi-account-multiple-check-outline"
|
||||||
|
class="q-ml-sm"
|
||||||
color="207 96% 32%"
|
color="207 96% 32%"
|
||||||
id="btn-select-invoice"
|
id="btn-select-invoice"
|
||||||
@click="
|
@click="
|
||||||
|
|
|
||||||
|
|
@ -86,10 +86,22 @@ const summaryPrice = ref<SummaryPrice>({
|
||||||
finalPrice: 0,
|
finalPrice: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchQuotationById(id: string) {
|
async function fetchQuotationById(id: string, codeInvoice: string) {
|
||||||
const res = await quotationStore.getQuotation(id);
|
const res = await quotationStore.getQuotation(id);
|
||||||
|
if (res) {
|
||||||
|
const installmentNo = res.paySplit.find(
|
||||||
|
(v) => codeInvoice === v.invoice?.code,
|
||||||
|
)?.no;
|
||||||
|
|
||||||
if (res) data.value = res;
|
data.value = {
|
||||||
|
...res,
|
||||||
|
productServiceList: !!installmentNo
|
||||||
|
? res.productServiceList.filter(
|
||||||
|
(v) => installmentNo === v.installmentNo,
|
||||||
|
)
|
||||||
|
: res.productServiceList,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAttachment(quotationId: string) {
|
async function getAttachment(quotationId: string) {
|
||||||
|
|
@ -190,7 +202,8 @@ onMounted(async () => {
|
||||||
if (data.value) {
|
if (data.value) {
|
||||||
if (!!data.value.id) {
|
if (!!data.value.id) {
|
||||||
await getAttachment(data.value.id);
|
await getAttachment(data.value.id);
|
||||||
await fetchQuotationById(data.value.id);
|
if (parsed.data.fetch)
|
||||||
|
await fetchQuotationById(data.value.id, parsed.data.codeInvoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resCustomerBranch = await customerStore.getBranchById(
|
const resCustomerBranch = await customerStore.getBranchById(
|
||||||
|
|
@ -203,8 +216,15 @@ onMounted(async () => {
|
||||||
|
|
||||||
agentPrice.value = data.value.agentPrice || parsed?.meta?.agentPrice;
|
agentPrice.value = data.value.agentPrice || parsed?.meta?.agentPrice;
|
||||||
|
|
||||||
|
const currentCode =
|
||||||
|
view.value === View.Invoice
|
||||||
|
? parsed.data.codeInvoice
|
||||||
|
: view.value === View.Payment
|
||||||
|
? parsed.data.codePayment
|
||||||
|
: (parsed?.meta?.source?.code ?? data.value?.code);
|
||||||
|
|
||||||
details.value = {
|
details.value = {
|
||||||
code: parsed?.meta?.source?.code ?? data.value?.code,
|
code: currentCode,
|
||||||
createdAt:
|
createdAt:
|
||||||
parsed?.meta?.source?.createdAt ??
|
parsed?.meta?.source?.createdAt ??
|
||||||
new Date(data.value?.createdAt || ''),
|
new Date(data.value?.createdAt || ''),
|
||||||
|
|
|
||||||
|
|
@ -88,12 +88,16 @@ function triggerView(opts: { quotationId: string }) {
|
||||||
window.open(url.toString(), '_blank');
|
window.open(url.toString(), '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewDocExample(quotationId: string) {
|
function viewDocExample(quotationId: string, codeInvoice: string) {
|
||||||
|
console.log(codeInvoice);
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'quotation-preview',
|
'quotation-preview',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
data: {
|
data: {
|
||||||
id: quotationId,
|
id: quotationId,
|
||||||
|
codeInvoice,
|
||||||
|
fetch: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -346,7 +350,7 @@ watch(
|
||||||
:rows="data"
|
:rows="data"
|
||||||
:grid="pageState.gridView"
|
:grid="pageState.gridView"
|
||||||
@view="(quotationId) => triggerView({ quotationId })"
|
@view="(quotationId) => triggerView({ quotationId })"
|
||||||
@preview="(quotationId) => viewDocExample(quotationId)"
|
@preview="(item) => viewDocExample(item.quotation.id, item.code)"
|
||||||
>
|
>
|
||||||
<template #grid="{ item }">
|
<template #grid="{ item }">
|
||||||
<div class="col-md-4 col-sm-6 col-12">
|
<div class="col-md-4 col-sm-6 col-12">
|
||||||
|
|
@ -401,9 +405,7 @@ watch(
|
||||||
() => triggerView({ quotationId: item.row.quotation.id })
|
() => triggerView({ quotationId: item.row.quotation.id })
|
||||||
"
|
"
|
||||||
@preview="
|
@preview="
|
||||||
() => {
|
() => viewDocExample(item.row.quotation.id, item.row.code)
|
||||||
viewDocExample(item.row.quotation.id);
|
|
||||||
}
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ withDefaults(
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'view', id: string): void;
|
(e: 'view', id: string): void;
|
||||||
(e: 'preview', id: string): void;
|
(e: 'preview', data: Invoice): void;
|
||||||
(e: 'edit', data: T, index: number): void;
|
(e: 'edit', data: T, index: number): void;
|
||||||
(e: 'delete', data: T, index: number): void;
|
(e: 'delete', data: T, index: number): void;
|
||||||
(e: 'download', data: T, index: number): void;
|
(e: 'download', data: T, index: number): void;
|
||||||
|
|
@ -118,7 +118,7 @@ defineEmits<{
|
||||||
icon="mdi-play-box-outline"
|
icon="mdi-play-box-outline"
|
||||||
size="12px"
|
size="12px"
|
||||||
:title="$t('preview.doc')"
|
:title="$t('preview.doc')"
|
||||||
@click.stop="$emit('preview', props.row.quotation.id)"
|
@click.stop="$emit('preview', props.row)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// NOTE: Library
|
// NOTE: Library
|
||||||
import { computed, onMounted, reactive, watch } from 'vue';
|
import { ref, computed, onMounted, reactive, watch } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { getRole } from 'src/services/keycloak';
|
import { getRole } from 'src/services/keycloak';
|
||||||
|
|
||||||
|
|
@ -19,27 +19,58 @@ import {
|
||||||
colReport,
|
colReport,
|
||||||
colReportProduct,
|
colReportProduct,
|
||||||
colReportSale,
|
colReportSale,
|
||||||
|
colProfit,
|
||||||
|
colProfitByMoth,
|
||||||
|
colProfitByYear,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import useFlowStore from 'src/stores/flow';
|
import useFlowStore from 'src/stores/flow';
|
||||||
import { useReportStore } from 'src/stores/report';
|
import { useReportStore } from 'src/stores/report';
|
||||||
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
import Expansion from 'src/components/14_report/Expansion.vue';
|
import Expansion from 'src/components/14_report/Expansion.vue';
|
||||||
|
import { SaveButton } from 'src/components/button';
|
||||||
|
import { ReportProfit } from 'src/stores/report/types';
|
||||||
|
|
||||||
// NOTE: Variable
|
// NOTE: Variable
|
||||||
const navigatorStore = useNavigator();
|
const navigatorStore = useNavigator();
|
||||||
const reportStore = useReportStore();
|
const reportStore = useReportStore();
|
||||||
const flow = useFlowStore();
|
const flow = useFlowStore();
|
||||||
|
|
||||||
|
const dataReportProfitByYears = ref<ReportProfit['dataset']>([]);
|
||||||
|
const pastYears = ref<number>(5);
|
||||||
|
const optionsPastYears = ref<number[]>([5, 10, 20, 30, 50, 80]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dataReportQuotation,
|
dataReportQuotation,
|
||||||
dataReportInvoice,
|
dataReportInvoice,
|
||||||
dataReportReceipt,
|
dataReportReceipt,
|
||||||
dataReportSale,
|
dataReportSale,
|
||||||
dataReportProduct,
|
dataReportProduct,
|
||||||
|
dataReportProfit,
|
||||||
} = storeToRefs(reportStore);
|
} = storeToRefs(reportStore);
|
||||||
|
|
||||||
const userRoles = computed(() => getRole() || []);
|
const userRoles = computed(() => getRole() || []);
|
||||||
|
|
||||||
|
const combinedProfitYear = computed(() => {
|
||||||
|
return (
|
||||||
|
dataReportProfitByYears.value?.reduce<ReportProfit['dataset']>(
|
||||||
|
(acc, val) => {
|
||||||
|
let find = acc.find((item) => item.year === val.year);
|
||||||
|
|
||||||
|
if (find) {
|
||||||
|
find.expenses += val.expenses;
|
||||||
|
find.netProfit += val.netProfit;
|
||||||
|
find.income += val.income;
|
||||||
|
} else {
|
||||||
|
acc.push({ ...val });
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
async function fetchReportQuotation() {
|
async function fetchReportQuotation() {
|
||||||
dataReportQuotation.value = (await reportStore.getReportQuotation()) || [];
|
dataReportQuotation.value = (await reportStore.getReportQuotation()) || [];
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +88,16 @@ async function fetchReportProduct() {
|
||||||
dataReportProduct.value = (await reportStore.getReportProduct()) || [];
|
dataReportProduct.value = (await reportStore.getReportProduct()) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchReportProfit() {
|
||||||
|
dataReportProfit.value = (await reportStore.getReportProfit()) || undefined;
|
||||||
|
dataReportProfitByYears.value = dataReportProfit.value?.dataset || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchReportProfitByYears(years: number) {
|
||||||
|
const res = (await reportStore.getReportProfit({ years })) || undefined;
|
||||||
|
dataReportProfitByYears.value = res?.dataset || [];
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
navigatorStore.current.title = 'report.title';
|
navigatorStore.current.title = 'report.title';
|
||||||
navigatorStore.current.path = [{ text: '' }];
|
navigatorStore.current.path = [{ text: '' }];
|
||||||
|
|
@ -64,7 +105,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
const isExecutive = computed(() => {
|
const isExecutive = computed(() => {
|
||||||
const roles = userRoles.value;
|
const roles = userRoles.value;
|
||||||
return roles.includes('executive');
|
return roles.includes('executive') || roles.includes('system');
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredTabs = computed(() => {
|
const filteredTabs = computed(() => {
|
||||||
|
|
@ -78,7 +119,7 @@ const filteredTabs = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageState = reactive({
|
const pageState = reactive({
|
||||||
currentTab: isExecutive.value ? ViewMode.Product : ViewMode.Document,
|
currentTab: ViewMode.Document,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchReportTab() {
|
async function fetchReportTab() {
|
||||||
|
|
@ -101,6 +142,10 @@ async function fetchReportTab() {
|
||||||
await fetchReportSale();
|
await fetchReportSale();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case ViewMode.Profit: {
|
||||||
|
await fetchReportProfit();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +156,10 @@ onMounted(async () => {
|
||||||
watch([() => pageState.currentTab], async () => {
|
watch([() => pageState.currentTab], async () => {
|
||||||
await fetchReportTab();
|
await fetchReportTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch([() => pastYears.value], async () => {
|
||||||
|
await fetchReportProfitByYears(pastYears.value);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -166,11 +215,33 @@ watch([() => pageState.currentTab], async () => {
|
||||||
<!-- Quotatio -->
|
<!-- Quotatio -->
|
||||||
|
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>{{ $t('quotation.title') }}</template>
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('quotation.title') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="reportStore.downloadReportQuotation()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport
|
<TableReport
|
||||||
:row="dataReportQuotation"
|
:row="
|
||||||
|
dataReportQuotation.map((v) => ({
|
||||||
|
...v,
|
||||||
|
name:
|
||||||
|
v.customerBranch.customer.customerType === 'CORP'
|
||||||
|
? $i18n.locale === 'eng'
|
||||||
|
? v.customerBranch.registerNameEN
|
||||||
|
: v.customerBranch.registerName
|
||||||
|
: $i18n.locale === 'eng'
|
||||||
|
? `${v.customerBranch.firstNameEN} ${v.customerBranch.lastNameEN}`
|
||||||
|
: `${v.customerBranch.firstName} ${v.customerBranch.lastName}`,
|
||||||
|
}))
|
||||||
|
"
|
||||||
:columns="colReportQuotation"
|
:columns="colReportQuotation"
|
||||||
>
|
>
|
||||||
<template #status="{ item }">
|
<template #status="{ item }">
|
||||||
|
|
@ -185,9 +256,34 @@ watch([() => pageState.currentTab], async () => {
|
||||||
|
|
||||||
<!-- Invoice -->
|
<!-- Invoice -->
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>{{ $t('invoice.title') }}</template>
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('invoice.title') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="reportStore.downloadReportInvoice()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport :row="dataReportInvoice" :columns="colReport">
|
<TableReport
|
||||||
|
:row="
|
||||||
|
dataReportInvoice.map((v) => ({
|
||||||
|
...v,
|
||||||
|
name:
|
||||||
|
v.customerBranch.customer.customerType === 'CORP'
|
||||||
|
? $i18n.locale === 'eng'
|
||||||
|
? v.customerBranch.registerNameEN
|
||||||
|
: v.customerBranch.registerName
|
||||||
|
: $i18n.locale === 'eng'
|
||||||
|
? `${v.customerBranch.firstNameEN} ${v.customerBranch.lastNameEN}`
|
||||||
|
: `${v.customerBranch.firstName} ${v.customerBranch.lastName}`,
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:columns="colReport"
|
||||||
|
>
|
||||||
<template #status="{ item }">
|
<template #status="{ item }">
|
||||||
<BadgeComponent
|
<BadgeComponent
|
||||||
:title="$t(`invoice.status.${item.row.status}`)"
|
:title="$t(`invoice.status.${item.row.status}`)"
|
||||||
|
|
@ -200,9 +296,34 @@ watch([() => pageState.currentTab], async () => {
|
||||||
|
|
||||||
<!-- Receipt -->
|
<!-- Receipt -->
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>{{ $t('receipt.title') }}</template>
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('receipt.title') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="reportStore.downloadReportReceipt()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport :row="dataReportReceipt" :columns="colReport">
|
<TableReport
|
||||||
|
:row="
|
||||||
|
dataReportReceipt.map((v) => ({
|
||||||
|
...v,
|
||||||
|
name:
|
||||||
|
v.customerBranch.customer.customerType === 'CORP'
|
||||||
|
? $i18n.locale === 'eng'
|
||||||
|
? v.customerBranch.registerNameEN
|
||||||
|
: v.customerBranch.registerName
|
||||||
|
: $i18n.locale === 'eng'
|
||||||
|
? `${v.customerBranch.firstNameEN} ${v.customerBranch.lastNameEN}`
|
||||||
|
: `${v.customerBranch.firstName} ${v.customerBranch.lastName}`,
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:columns="colReport"
|
||||||
|
>
|
||||||
<template #status="{ item }">
|
<template #status="{ item }">
|
||||||
<BadgeComponent
|
<BadgeComponent
|
||||||
:title="$t(`invoice.status.${item.row.status}`)"
|
:title="$t(`invoice.status.${item.row.status}`)"
|
||||||
|
|
@ -217,9 +338,34 @@ watch([() => pageState.currentTab], async () => {
|
||||||
|
|
||||||
<template v-if="pageState.currentTab === ViewMode.Invoice">
|
<template v-if="pageState.currentTab === ViewMode.Invoice">
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>{{ $t('invoice.title') }}</template>
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('invoice.title') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="reportStore.downloadReportInvoice()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport :row="dataReportInvoice" :columns="colReport">
|
<TableReport
|
||||||
|
:row="
|
||||||
|
dataReportInvoice.map((v) => ({
|
||||||
|
...v,
|
||||||
|
name:
|
||||||
|
v.customerBranch.customer.customerType === 'CORP'
|
||||||
|
? $i18n.locale === 'eng'
|
||||||
|
? v.customerBranch.registerNameEN
|
||||||
|
: v.customerBranch.registerName
|
||||||
|
: $i18n.locale === 'eng'
|
||||||
|
? `${v.customerBranch.firstNameEN} ${v.customerBranch.lastNameEN}`
|
||||||
|
: `${v.customerBranch.firstName} ${v.customerBranch.lastName}`,
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:columns="colReport"
|
||||||
|
>
|
||||||
<template #status="{ item }">
|
<template #status="{ item }">
|
||||||
<BadgeComponent
|
<BadgeComponent
|
||||||
:title="$t(`invoice.status.${item.row.status}`)"
|
:title="$t(`invoice.status.${item.row.status}`)"
|
||||||
|
|
@ -233,7 +379,17 @@ watch([() => pageState.currentTab], async () => {
|
||||||
|
|
||||||
<template v-if="pageState.currentTab === ViewMode.Product">
|
<template v-if="pageState.currentTab === ViewMode.Product">
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>{{ $t('productService.title') }}</template>
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('productService.title') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="reportStore.downloadReportInvoice()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport
|
<TableReport
|
||||||
:row="dataReportProduct"
|
:row="dataReportProduct"
|
||||||
|
|
@ -245,7 +401,19 @@ watch([() => pageState.currentTab], async () => {
|
||||||
<template v-if="pageState.currentTab === ViewMode.Sale">
|
<template v-if="pageState.currentTab === ViewMode.Sale">
|
||||||
<div class="q-gutter-y-md">
|
<div class="q-gutter-y-md">
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>{{ $t('report.sale.byCustomer') }}</template>
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('report.sale.byCustomer') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="
|
||||||
|
reportStore.downloadReportSale('by-customer')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport
|
<TableReport
|
||||||
:row="
|
:row="
|
||||||
|
|
@ -254,7 +422,9 @@ watch([() => pageState.currentTab], async () => {
|
||||||
code: v.code,
|
code: v.code,
|
||||||
name:
|
name:
|
||||||
v.customer.customerType === 'CORP'
|
v.customer.customerType === 'CORP'
|
||||||
? v.customerName
|
? $i18n.locale === 'eng'
|
||||||
|
? v.registerNameEN
|
||||||
|
: v.registerName
|
||||||
: $i18n.locale === 'eng'
|
: $i18n.locale === 'eng'
|
||||||
? `${v.firstNameEN} ${v.lastNameEN}`
|
? `${v.firstNameEN} ${v.lastNameEN}`
|
||||||
: `${v.firstName} ${v.lastName}`,
|
: `${v.firstName} ${v.lastName}`,
|
||||||
|
|
@ -273,7 +443,17 @@ watch([() => pageState.currentTab], async () => {
|
||||||
</Expansion>
|
</Expansion>
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ $t('report.sale.byProductGroup') }}
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('report.sale.byProductGroup') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="
|
||||||
|
reportStore.downloadReportSale('by-product-group')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport
|
<TableReport
|
||||||
|
|
@ -295,7 +475,17 @@ watch([() => pageState.currentTab], async () => {
|
||||||
</template>
|
</template>
|
||||||
</Expansion>
|
</Expansion>
|
||||||
<Expansion default-opened>
|
<Expansion default-opened>
|
||||||
<template #header>{{ $t('report.sale.bySale') }}</template>
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('report.sale.bySale') }}
|
||||||
|
<SaveButton
|
||||||
|
style="margin-left: auto"
|
||||||
|
:icon="'material-symbols:download'"
|
||||||
|
:label="$t('general.download')"
|
||||||
|
@click.stop="reportStore.downloadReportSale('by-sale')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<TableReport
|
<TableReport
|
||||||
:row="
|
:row="
|
||||||
|
|
@ -353,6 +543,99 @@ watch([() => pageState.currentTab], async () => {
|
||||||
</Expansion>
|
</Expansion>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="pageState.currentTab === ViewMode.Profit">
|
||||||
|
<div class="q-gutter-y-md">
|
||||||
|
<Expansion default-opened>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('report.profit.byMonth') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #main>
|
||||||
|
<TableReport
|
||||||
|
:row="dataReportProfit?.dataset"
|
||||||
|
:columns="colProfitByMoth"
|
||||||
|
>
|
||||||
|
<template #title="{ item }">
|
||||||
|
{{ $t(`month.${item.row.month}`) }}
|
||||||
|
</template>
|
||||||
|
</TableReport>
|
||||||
|
</template>
|
||||||
|
</Expansion>
|
||||||
|
|
||||||
|
<Expansion default-opened>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('report.profit.byYear') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #main>
|
||||||
|
<div class="q-gutter-y-md">
|
||||||
|
<q-select
|
||||||
|
style="max-width: 150px"
|
||||||
|
outlined
|
||||||
|
use-input
|
||||||
|
fill-input
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
hide-selected
|
||||||
|
hide-bottom-space
|
||||||
|
input-debounce="0"
|
||||||
|
option-value="value"
|
||||||
|
option-label="label"
|
||||||
|
dense
|
||||||
|
v-model="pastYears"
|
||||||
|
:options="
|
||||||
|
optionsPastYears.map((v) => ({
|
||||||
|
value: v,
|
||||||
|
label: `${v} ${$t('general.year')}`,
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
label="เลือกปีย้อนหลัง"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<TableReport
|
||||||
|
:row="combinedProfitYear"
|
||||||
|
:columns="colProfitByYear"
|
||||||
|
></TableReport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Expansion>
|
||||||
|
|
||||||
|
<Expansion default-opened>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex full-width items-center">
|
||||||
|
{{ $t('report.view.Profit') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #main>
|
||||||
|
<TableReport
|
||||||
|
:row="[
|
||||||
|
{
|
||||||
|
title: $t('report.profit.netProfit'),
|
||||||
|
amount: dataReportProfit?.netProfit || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('report.profit.expenses'),
|
||||||
|
amount: dataReportProfit?.expenses || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('report.profit.income'),
|
||||||
|
amount: dataReportProfit?.income || 0,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:columns="colProfit"
|
||||||
|
hide-header
|
||||||
|
hide-bottom
|
||||||
|
></TableReport>
|
||||||
|
</template>
|
||||||
|
</Expansion>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ const prop = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
row: QTableProps['rows'];
|
row: QTableProps['rows'];
|
||||||
columns: QTableProps['columns'];
|
columns: QTableProps['columns'];
|
||||||
|
hideHeader?: boolean;
|
||||||
|
hideBottom?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
row: () => [],
|
row: () => [],
|
||||||
|
|
@ -22,6 +24,8 @@ const prop = withDefaults(
|
||||||
}))
|
}))
|
||||||
"
|
"
|
||||||
:columns
|
:columns
|
||||||
|
:hideHeader
|
||||||
|
:hideBottom
|
||||||
bordered
|
bordered
|
||||||
flat
|
flat
|
||||||
selection="multiple"
|
selection="multiple"
|
||||||
|
|
@ -32,7 +36,12 @@ const prop = withDefaults(
|
||||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||||
:props="props"
|
:props="props"
|
||||||
>
|
>
|
||||||
<q-th v-for="col in columns" :key="col.name" :props="props">
|
<q-th
|
||||||
|
v-for="col in columns"
|
||||||
|
style="text-align: center"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
{{ $t(col.label) }}
|
{{ $t(col.label) }}
|
||||||
</q-th>
|
</q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export enum ViewMode {
|
||||||
Receipt = 'receipt',
|
Receipt = 'receipt',
|
||||||
Product = 'product',
|
Product = 'product',
|
||||||
Sale = 'sale',
|
Sale = 'sale',
|
||||||
|
Profit = 'profit',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColumnsSale = {
|
type ColumnsSale = {
|
||||||
|
|
@ -28,6 +29,25 @@ type ColumnsBySale = ColumnsSale & {
|
||||||
gender?: string;
|
gender?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ColumnsProfix = {
|
||||||
|
title: string;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColumnsProfixByMonth = {
|
||||||
|
month: string;
|
||||||
|
netProfit: number;
|
||||||
|
expenses: number;
|
||||||
|
income: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColumnsProfixByYear = Omit<ColumnsProfixByMonth, 'moth'> & {
|
||||||
|
year: string;
|
||||||
|
netProfit: number;
|
||||||
|
expenses: number;
|
||||||
|
income: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const colReportQuotation = [
|
export const colReportQuotation = [
|
||||||
{
|
{
|
||||||
name: 'code',
|
name: 'code',
|
||||||
|
|
@ -41,6 +61,18 @@ export const colReportQuotation = [
|
||||||
label: 'report.document.status',
|
label: 'report.document.status',
|
||||||
field: '',
|
field: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'customer',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.customerName',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.value',
|
||||||
|
field: (data: ReportQuotation) => formatNumberDecimal(data.amount, 2),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'createAt',
|
name: 'createAt',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
|
@ -68,6 +100,18 @@ export const colReport = [
|
||||||
label: 'report.table.status',
|
label: 'report.table.status',
|
||||||
field: '',
|
field: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'customer',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.customerName',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.value',
|
||||||
|
field: (data: Report) => formatNumberDecimal(data.amount, 2),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'createAt',
|
name: 'createAt',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
|
@ -145,9 +189,81 @@ export const colReportBySale = [
|
||||||
},
|
},
|
||||||
] as const satisfies QTableProps['columns'];
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const colProfit = [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: (data: ColumnsProfix) => data.title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: (data: ColumnsProfix) => formatNumberDecimal(data.amount, 2),
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const colProfitByMoth = [
|
||||||
|
{
|
||||||
|
name: '#title',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.month',
|
||||||
|
field: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'netProfit',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.netProfit',
|
||||||
|
field: (data: ColumnsProfixByMonth) =>
|
||||||
|
formatNumberDecimal(data.netProfit, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expenses',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.expenses',
|
||||||
|
field: (data: ColumnsProfixByMonth) =>
|
||||||
|
formatNumberDecimal(data.expenses, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'income',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.income',
|
||||||
|
field: (data: ColumnsProfixByMonth) => formatNumberDecimal(data.income, 2),
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const colProfitByYear = [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.year',
|
||||||
|
field: (data: ColumnsProfixByYear) => data.year,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'netProfit',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.netProfit',
|
||||||
|
field: (data: ColumnsProfixByYear) =>
|
||||||
|
formatNumberDecimal(data.netProfit, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expenses',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.expenses',
|
||||||
|
field: (data: ColumnsProfixByYear) => formatNumberDecimal(data.expenses, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'income',
|
||||||
|
align: 'left',
|
||||||
|
label: 'report.profit.income',
|
||||||
|
field: (data: ColumnsProfixByYear) => formatNumberDecimal(data.income, 2),
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
export const pageTabs = [
|
export const pageTabs = [
|
||||||
{ label: 'Document', value: ViewMode.Document, by: ['user'] },
|
{ label: 'Document', value: ViewMode.Document, by: ['user'] },
|
||||||
{ label: 'Invoice', value: ViewMode.Invoice, by: ['user'] },
|
{ label: 'Invoice', value: ViewMode.Invoice, by: ['user'] },
|
||||||
{ label: 'Product', value: ViewMode.Product, by: ['user'] },
|
{ label: 'Product', value: ViewMode.Product, by: ['user'] },
|
||||||
{ label: 'Sale', value: ViewMode.Sale, by: ['admin'] },
|
{ label: 'Sale', value: ViewMode.Sale, by: ['admin'] },
|
||||||
|
{ label: 'Profit', value: ViewMode.Profit, by: ['admin'] },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// NOTE: Library
|
// NOTE: Library
|
||||||
import { onMounted, reactive } from 'vue';
|
import { onMounted, reactive, watch } from 'vue';
|
||||||
|
|
||||||
// NOTE: Components
|
// NOTE: Components
|
||||||
import SelectInput from 'src/components/shared/SelectInput.vue';
|
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||||
import ChartReceipt from './chart/ChartReceipt.vue';
|
import ChartReceipt from './chart/ChartReceipt.vue';
|
||||||
import ChartOpportunity from './chart/ChartOpportunity.vue';
|
|
||||||
import ChartQuotationStatus from './chart/ChartQuotationStatus.vue';
|
import ChartQuotationStatus from './chart/ChartQuotationStatus.vue';
|
||||||
import ChartSales from './chart/ChartSales.vue';
|
import VueDatePicker from '@vuepic/vue-datepicker';
|
||||||
|
// import ChartOpportunity from './chart/ChartOpportunity.vue';
|
||||||
|
// import ChartSales from './chart/ChartSales.vue';
|
||||||
|
|
||||||
// NOTE: Stores & Type
|
// NOTE: Stores & Type
|
||||||
import { useNavigator } from 'src/stores/navigator';
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
|
|
@ -16,6 +17,7 @@ import { useQuotationStore } from 'src/stores/quotations';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useReportStore } from 'src/stores/report';
|
import { useReportStore } from 'src/stores/report';
|
||||||
import { PaymentDataStatus } from 'src/stores/payment/types';
|
import { PaymentDataStatus } from 'src/stores/payment/types';
|
||||||
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
|
||||||
// NOTE: Variable
|
// NOTE: Variable
|
||||||
const navigatorStore = useNavigator();
|
const navigatorStore = useNavigator();
|
||||||
|
|
@ -24,57 +26,101 @@ const reportStore = useReportStore();
|
||||||
const { stats: quotationStats } = storeToRefs(quotationStore);
|
const { stats: quotationStats } = storeToRefs(quotationStore);
|
||||||
const { dataReportPayment } = storeToRefs(reportStore);
|
const { dataReportPayment } = storeToRefs(reportStore);
|
||||||
|
|
||||||
|
const endDate = new Date();
|
||||||
|
const startDate = new Date(new Date().setFullYear(endDate.getFullYear() - 1));
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
year: 'now',
|
date: [startDate, endDate],
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: mock
|
// const option = reactive({
|
||||||
const option = reactive({
|
// role: [
|
||||||
role: [
|
// { label: 'ผู้ดูแล', value: 'admin' },
|
||||||
{ label: 'ผู้ดูแล', value: 'admin' },
|
// { label: 'ผู้ใช้', value: 'user' },
|
||||||
{ label: 'ผู้ใช้', value: 'user' },
|
// ],
|
||||||
],
|
// });
|
||||||
year: [
|
|
||||||
{ label: 'ปีนี้', value: 'now' },
|
async function fetchData() {
|
||||||
{ label: '2024', value: '2024' },
|
const ret = await quotationStore.getQuotationStats({
|
||||||
],
|
startDate: state.date[0],
|
||||||
});
|
endDate: state.date[1],
|
||||||
|
});
|
||||||
|
if (ret) {
|
||||||
|
quotationStats.value = {
|
||||||
|
issued: 0,
|
||||||
|
accepted: 0,
|
||||||
|
expired: 0,
|
||||||
|
paymentInProcess: 0,
|
||||||
|
paymentSuccess: 0,
|
||||||
|
processComplete: 0,
|
||||||
|
canceled: 0,
|
||||||
|
};
|
||||||
|
quotationStats.value = Object.assign(quotationStats.value, ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
const retPayment = await reportStore.getReportPayment({
|
||||||
|
startDate: state.date[0],
|
||||||
|
endDate: state.date[1],
|
||||||
|
});
|
||||||
|
if (retPayment) {
|
||||||
|
dataReportPayment.value = retPayment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
navigatorStore.current.title = 'dashboard.title';
|
navigatorStore.current.title = 'dashboard.title';
|
||||||
navigatorStore.current.path = [{ text: '' }];
|
navigatorStore.current.path = [{ text: '' }];
|
||||||
|
|
||||||
const ret = await quotationStore.getQuotationStats();
|
await fetchData();
|
||||||
if (ret) {
|
|
||||||
quotationStats.value = Object.assign(quotationStats.value, ret);
|
|
||||||
console.log(quotationStats.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const retPayment = await reportStore.getReportPayment();
|
|
||||||
|
|
||||||
if (retPayment) {
|
|
||||||
dataReportPayment.value = retPayment;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.date,
|
||||||
|
async () => {
|
||||||
|
await fetchData();
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="column full-height no-wrap">
|
<div class="column full-height no-wrap">
|
||||||
<header class="row q-gutter-sm">
|
<header class="row q-gutter-sm">
|
||||||
<SelectInput
|
<!-- <SelectInput
|
||||||
class="col-md-3 col"
|
class="col-md-3 col"
|
||||||
:option="option.role"
|
:option="option.role"
|
||||||
v-model="state.role"
|
v-model="state.role"
|
||||||
></SelectInput>
|
for="selecte-role"
|
||||||
<SelectInput
|
></SelectInput> -->
|
||||||
class="col-md-2 col"
|
<VueDatePicker
|
||||||
:option="option.year"
|
utc
|
||||||
v-model="state.year"
|
range
|
||||||
|
teleport
|
||||||
|
auto-apply
|
||||||
|
for="select-date-range"
|
||||||
|
class="col-md-4 col"
|
||||||
|
v-model="state.date"
|
||||||
|
:dark="$q.dark.isActive"
|
||||||
|
:locale="$i18n.locale === 'tha' ? 'th' : 'en'"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #trigger>
|
||||||
<q-icon name="mdi-calendar-outline app-text-muted" />
|
<q-input
|
||||||
|
placeholder="DD/MM/YYYY"
|
||||||
|
hide-bottom-space
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
for="select-date-range"
|
||||||
|
:model-value="
|
||||||
|
dateFormatJS({ date: state.date[0] }) +
|
||||||
|
' - ' +
|
||||||
|
dateFormatJS({ date: state.date[1] })
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-calendar-outline" class="app-text-muted" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
</template>
|
</template>
|
||||||
</SelectInput>
|
</VueDatePicker>
|
||||||
|
|
||||||
<div class="col-12 scroll">
|
<div class="col-12 scroll">
|
||||||
<div style="display: inline-block">
|
<div style="display: inline-block">
|
||||||
|
|
|
||||||
|
|
@ -105,3 +105,9 @@ const chartOptions = computed(() => ({
|
||||||
></VueApexCharts>
|
></VueApexCharts>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.apexcharts-menu.apexcharts-menu-open) {
|
||||||
|
background: var(--surface-1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -23,21 +23,6 @@ const prop = withDefaults(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const chartOptions = computed(() => {
|
const chartOptions = computed(() => {
|
||||||
const thaiMonths = [
|
|
||||||
'ม.ค.',
|
|
||||||
'ก.พ.',
|
|
||||||
'มี.ค.',
|
|
||||||
'เม.ย.',
|
|
||||||
'พ.ค.',
|
|
||||||
'มิ.ย.',
|
|
||||||
'ก.ค.',
|
|
||||||
'ส.ค.',
|
|
||||||
'ก.ย.',
|
|
||||||
'ต.ค.',
|
|
||||||
'พ.ย.',
|
|
||||||
'ธ.ค.',
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
colors: ['#035aa1', '#ae3ec9', '#ffa94d', '#e64980'],
|
colors: ['#035aa1', '#ae3ec9', '#ffa94d', '#e64980'],
|
||||||
chart: {
|
chart: {
|
||||||
|
|
@ -47,6 +32,9 @@ const chartOptions = computed(() => {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tooltip: {
|
||||||
|
theme: 'dark',
|
||||||
|
},
|
||||||
legend: {
|
legend: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
|
|
@ -109,3 +97,9 @@ const detail = [
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.apexcharts-menu.apexcharts-menu-open) {
|
||||||
|
background: var(--surface-1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'dashBoard',
|
name: 'dashBoard',
|
||||||
component: () => import('pages/15_dash-board/MainPage.vue'),
|
component: () => import('pages/15_dash-board/MainPage.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/notification',
|
||||||
|
name: 'Notification',
|
||||||
|
component: () => import('pages/00_notification/MainPage.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -208,7 +213,6 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'DebitNoteView',
|
name: 'DebitNoteView',
|
||||||
component: () => import('pages/12_debit-note/FormPage.vue'),
|
component: () => import('pages/12_debit-note/FormPage.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/debit-note/document-view',
|
path: '/debit-note/document-view',
|
||||||
name: 'DebitNoteDocumentView',
|
name: 'DebitNoteDocumentView',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@ import { api } from 'src/boot/axios';
|
||||||
import { PaginationResult } from 'src/types';
|
import { PaginationResult } from 'src/types';
|
||||||
import { createDataRefBase } from '../utils';
|
import { createDataRefBase } from '../utils';
|
||||||
|
|
||||||
export type Notification = {};
|
export type Notification = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
read?: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const useNotification = defineStore('noti-store', () => {
|
export const useNotification = defineStore('noti-store', () => {
|
||||||
const state = createDataRefBase<Notification>();
|
const state = createDataRefBase<Notification>();
|
||||||
|
|
@ -28,9 +34,12 @@ export const useNotification = defineStore('noti-store', () => {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNotification(paymentId: string, payload: Notification) {
|
async function updateNotification(
|
||||||
|
notificationId: string,
|
||||||
|
payload: Notification,
|
||||||
|
) {
|
||||||
const res = await api.put<Notification & { id: string }>(
|
const res = await api.put<Notification & { id: string }>(
|
||||||
`/notification/${paymentId}`,
|
`/notification/${notificationId}`,
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
if (res.status >= 400) return null;
|
if (res.status >= 400) return null;
|
||||||
|
|
@ -45,6 +54,26 @@ export const useNotification = defineStore('noti-store', () => {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMultiNotification(notificationId: string[]) {
|
||||||
|
const res = await api.delete<Notification & { id: string }>(
|
||||||
|
'/notification',
|
||||||
|
{
|
||||||
|
data: notificationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.status >= 400) return null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markReadNotification(notificationId: string[]) {
|
||||||
|
const res = await api.post<Notification & { id: string }>(
|
||||||
|
'/notification/mark-read',
|
||||||
|
{ id: notificationId },
|
||||||
|
);
|
||||||
|
if (res.status >= 400) return null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
||||||
|
|
@ -52,5 +81,7 @@ export const useNotification = defineStore('noti-store', () => {
|
||||||
getNotificationList,
|
getNotificationList,
|
||||||
updateNotification,
|
updateNotification,
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
|
deleteMultiNotification,
|
||||||
|
markReadNotification,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,11 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
||||||
canceled: 0,
|
canceled: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getQuotationStats() {
|
async function getQuotationStats(params?: {
|
||||||
const res = await api.get<QuotationStats>('/quotation/stats');
|
startDate: string | Date;
|
||||||
|
endDate: string | Date;
|
||||||
|
}) {
|
||||||
|
const res = await api.get<QuotationStats>('/quotation/stats', { params });
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,7 @@ export type QuotationPaymentData = {
|
||||||
remark: string;
|
remark: string;
|
||||||
date: string;
|
date: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Details = {
|
export type Details = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { Pagination, Status } from '../types';
|
|
||||||
import { api } from 'src/boot/axios';
|
import { api } from 'src/boot/axios';
|
||||||
import {
|
import {
|
||||||
Report,
|
Report,
|
||||||
|
|
@ -8,10 +7,35 @@ import {
|
||||||
ReportProduct,
|
ReportProduct,
|
||||||
ReportQuotation,
|
ReportQuotation,
|
||||||
ReportSale,
|
ReportSale,
|
||||||
|
ReportProfit,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { baseUrl } from '../utils';
|
||||||
|
import { getToken } from 'src/services/keycloak';
|
||||||
|
|
||||||
const ENDPOINT = 'report';
|
const ENDPOINT = 'report';
|
||||||
|
|
||||||
|
async function _download(url: string, filename?: string) {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { ['Authorization']: 'Bearer ' + (await getToken()) },
|
||||||
|
});
|
||||||
|
const text = await res.json();
|
||||||
|
const blob = new Blob([text], { type: 'text/csv' });
|
||||||
|
if (res.ok && blob) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.download = (filename || 'report') + '.csv';
|
||||||
|
a.href = window.URL.createObjectURL(blob);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadReportQuotation() {
|
||||||
|
await _download(
|
||||||
|
baseUrl + '/' + ENDPOINT + '/quotation/download',
|
||||||
|
'quotation-report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getReportQuotation() {
|
export async function getReportQuotation() {
|
||||||
const res = await api.get<ReportQuotation[]>(`/${ENDPOINT}/quotation`);
|
const res = await api.get<ReportQuotation[]>(`/${ENDPOINT}/quotation`);
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
|
|
@ -21,6 +45,13 @@ export async function getReportQuotation() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadReportInvoice() {
|
||||||
|
await _download(
|
||||||
|
baseUrl + '/' + ENDPOINT + '/invoice/download',
|
||||||
|
'invoice-report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getReportInvoice() {
|
export async function getReportInvoice() {
|
||||||
const res = await api.get<Report[]>(`/${ENDPOINT}/invoice`);
|
const res = await api.get<Report[]>(`/${ENDPOINT}/invoice`);
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
|
|
@ -29,6 +60,13 @@ export async function getReportInvoice() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadReportReceipt() {
|
||||||
|
await _download(
|
||||||
|
baseUrl + '/' + ENDPOINT + '/receipt/download',
|
||||||
|
'receipt-report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getReportReceipt() {
|
export async function getReportReceipt() {
|
||||||
const res = await api.get<Report[]>(`/${ENDPOINT}/receipt`);
|
const res = await api.get<Report[]>(`/${ENDPOINT}/receipt`);
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
|
|
@ -37,6 +75,15 @@ export async function getReportReceipt() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadReportSale(
|
||||||
|
category: 'by-product-group' | 'by-sale' | 'by-customer',
|
||||||
|
) {
|
||||||
|
await _download(
|
||||||
|
baseUrl + '/' + ENDPOINT + '/sale/' + category + '/download',
|
||||||
|
'sale-' + category + '-report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getReportSale() {
|
export async function getReportSale() {
|
||||||
const res = await api.get<ReportSale>(`/${ENDPOINT}/sale`);
|
const res = await api.get<ReportSale>(`/${ENDPOINT}/sale`);
|
||||||
|
|
||||||
|
|
@ -46,16 +93,53 @@ export async function getReportSale() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadReportProduct() {
|
||||||
|
await _download(
|
||||||
|
baseUrl + '/' + ENDPOINT + '/receipt/download',
|
||||||
|
'product-report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getReportProduct() {
|
export async function getReportProduct() {
|
||||||
const res = await api.get<ReportProduct[]>(`/${ENDPOINT}/Product`);
|
const res = await api.get<ReportProduct[]>(`/${ENDPOINT}/product`);
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReportPayment() {
|
export async function getReportPayment(params?: {
|
||||||
const res = await api.get<ReportPayment[]>(`/${ENDPOINT}/payment`);
|
startDate: string | Date;
|
||||||
|
endDate: string | Date;
|
||||||
|
}) {
|
||||||
|
const res = await api.get<ReportPayment[]>(`/${ENDPOINT}/payment`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReportProfit(params?: {
|
||||||
|
years?: number;
|
||||||
|
startDate?: string | Date;
|
||||||
|
endDate?: string | Date;
|
||||||
|
}) {
|
||||||
|
let opts = params || {};
|
||||||
|
|
||||||
|
if (params?.years) {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
opts.startDate = new Date(currentYear - params.years, 0, 1);
|
||||||
|
opts.endDate = new Date(currentYear, 11, 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.get<ReportProfit>(`/${ENDPOINT}/profit`, {
|
||||||
|
params: {
|
||||||
|
startDate: opts.startDate,
|
||||||
|
endDate: opts.endDate,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +153,7 @@ export const useReportStore = defineStore('report-store', () => {
|
||||||
const dataReportSale = ref<ReportSale>();
|
const dataReportSale = ref<ReportSale>();
|
||||||
const dataReportProduct = ref<ReportProduct[]>([]);
|
const dataReportProduct = ref<ReportProduct[]>([]);
|
||||||
const dataReportPayment = ref<ReportPayment[]>([]);
|
const dataReportPayment = ref<ReportPayment[]>([]);
|
||||||
|
const dataReportProfit = ref<ReportProfit>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataReportQuotation,
|
dataReportQuotation,
|
||||||
|
|
@ -77,12 +162,19 @@ export const useReportStore = defineStore('report-store', () => {
|
||||||
dataReportSale,
|
dataReportSale,
|
||||||
dataReportProduct,
|
dataReportProduct,
|
||||||
dataReportPayment,
|
dataReportPayment,
|
||||||
|
dataReportProfit,
|
||||||
|
|
||||||
|
downloadReportQuotation,
|
||||||
getReportQuotation,
|
getReportQuotation,
|
||||||
|
downloadReportInvoice,
|
||||||
getReportInvoice,
|
getReportInvoice,
|
||||||
|
downloadReportReceipt,
|
||||||
getReportReceipt,
|
getReportReceipt,
|
||||||
|
downloadReportSale,
|
||||||
getReportSale,
|
getReportSale,
|
||||||
|
downloadReportProduct,
|
||||||
getReportProduct,
|
getReportProduct,
|
||||||
getReportPayment,
|
getReportPayment,
|
||||||
|
getReportProfit,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import { CustomerBranch } from '../customer';
|
||||||
import { PaymentDataStatus } from '../payment/types';
|
import { PaymentDataStatus } from '../payment/types';
|
||||||
|
|
||||||
export type ReportQuotation = {
|
export type ReportQuotation = {
|
||||||
|
customerBranch: CustomerBranch;
|
||||||
updatedAt: Date | null;
|
updatedAt: Date | null;
|
||||||
createdAt: Date | null;
|
createdAt: Date | null;
|
||||||
|
amount: number;
|
||||||
status: QuotationStatus;
|
status: QuotationStatus;
|
||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
@ -20,7 +22,9 @@ export enum Status {
|
||||||
|
|
||||||
// use with Invoice and Receipt
|
// use with Invoice and Receipt
|
||||||
export type Report = {
|
export type Report = {
|
||||||
|
customerBranch: CustomerBranch;
|
||||||
createdAt: Date | null;
|
createdAt: Date | null;
|
||||||
|
amount: number;
|
||||||
status: Status;
|
status: Status;
|
||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
@ -46,3 +50,16 @@ export type ReportSale = {
|
||||||
bySale: (User & { _count: number })[];
|
bySale: (User & { _count: number })[];
|
||||||
byProductGroup: (Omit<ProductGroup, '_count'> & { _count: number })[];
|
byProductGroup: (Omit<ProductGroup, '_count'> & { _count: number })[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReportProfit = {
|
||||||
|
dataset: {
|
||||||
|
netProfit: number;
|
||||||
|
expenses: number;
|
||||||
|
income: number;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
}[];
|
||||||
|
netProfit: number;
|
||||||
|
expenses: number;
|
||||||
|
income: number;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -532,7 +532,7 @@ export function createDataRefBase<T>(
|
||||||
defaultPageSize = 30,
|
defaultPageSize = 30,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
data: ref<T[]>(),
|
data: ref<T[]>([]),
|
||||||
page: ref<number>(defaultPage),
|
page: ref<number>(defaultPage),
|
||||||
pageMax: ref<number>(defaultPageMax),
|
pageMax: ref<number>(defaultPageMax),
|
||||||
pageSize: ref<number>(defaultPageSize),
|
pageSize: ref<number>(defaultPageSize),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue