Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s

This commit is contained in:
Methapon2001 2025-03-10 08:43:22 +07:00
commit 2b35db5683
28 changed files with 1249 additions and 191 deletions

View file

@ -47,7 +47,7 @@ const rand = Math.random();
</script>
<template>
<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 }"
:style="{ '--animation-delay': rand + 's' }"
>
@ -110,8 +110,8 @@ const rand = Math.random();
</nav>
</header>
<div class="ellipsis q-px-xs">
<b>{{ title || '-' }}</b>
<div class="text-bold q-px-xs ellipsis full-width">
{{ title || '-' }}
<q-tooltip anchor="bottom start" self="top left">
{{ title || '-' }}
</q-tooltip>

View file

@ -18,6 +18,7 @@ const props = withDefaults(
hideEdit?: boolean;
page?: number;
pageSize?: number;
hideBtnPreview?: boolean;
}>(),
{
row: () => [],
@ -127,6 +128,7 @@ defineEmits<{
<q-td class="text-right">
<q-btn
v-if="!hideBtnPreview"
:id="`btn-preview-${props.row.workName}`"
icon="mdi-play-box-outline"
size="sm"

View file

@ -15,9 +15,7 @@ defineProps<{
:default-opened="defaultOpened"
>
<template #header>
<span>
<slot name="header" />
</span>
<slot name="header" />
</template>
<main class="surface-1 q-pa-md full-width">

View file

@ -85,6 +85,10 @@ div.fullscreen.q-drawer__backdrop {
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],

View file

@ -149,6 +149,7 @@ export default {
included: 'Included',
notIncluded: 'Not Included',
dueDate: 'Due date',
year: 'year',
},
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: {
tm6: {
transportation: 'Flight/Vehicle',
@ -791,7 +802,7 @@ export default {
paySplitMessage: 'Amount to be paid (Baht)',
summary: 'Total Summary',
periodNo: 'Installment No."',
periodNo: 'Installment No.',
amount: 'Amount',
payDueDate: 'Pay Due Date',
callDueDate: 'Call Due Date',
@ -1141,6 +1152,7 @@ export default {
taskListNotFound: 'Task list cannot be found.',
creditNoteNotFound: 'Credit note cannot be found.',
debitNoteNotFound: 'Debit note cannot be found.',
notificationNotFound: 'Notification cannot be found.',
productGroupIsUsed: 'Product group is in used.',
productIsUsed: 'Product is in used.',
@ -1339,39 +1351,49 @@ export default {
},
},
report: {
report: {
title: 'Report',
view: {
Document: 'Document Status Report',
Invoice: 'Payment Report',
Product: 'Product and Service Report',
Sale: 'Sales Summary Report',
},
document: {
code: 'Code',
status: 'Quotation Status',
createAt: 'Created Date',
updateAt: 'Updated Date',
},
table: {
code: 'Code',
status: 'Status',
createAt: 'Created Date',
},
product: {
did: 'Processed Quantity',
sale: 'Sold Quantity',
name: 'Product Name',
code: 'Product Code',
},
sale: {
byCustomer: 'Sales by Branch',
byProductGroup: 'Sales by Product Category',
bySale: 'Sales by Salesperson',
code: 'Code',
name: 'Name',
count: 'Sold Quantity',
},
title: 'Report',
value: 'Value',
customerName: 'Customer Name',
view: {
Document: 'Document Status Report',
Invoice: 'Payment Report',
Product: 'Product and Service Report',
Sale: 'Sales Summary Report',
Profit: 'Profit and Loss Report',
},
document: {
code: 'Code',
status: 'Quotation Status',
createAt: 'Created Date',
updateAt: 'Updated Date',
},
table: {
code: 'Code',
status: 'Status',
createAt: 'Created Date',
},
product: {
did: 'Processed Quantity',
sale: 'Sold Quantity',
name: 'Product Name',
code: 'Product Code',
},
sale: {
byCustomer: 'Sales by Branch',
byProductGroup: 'Sales by Product Category',
bySale: 'Sales by Salesperson',
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: {
@ -1406,4 +1428,19 @@ export default {
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',
},
};

View file

@ -149,6 +149,7 @@ export default {
included: 'รวม',
notIncluded: 'ไม่รวม',
dueDate: 'วันครบกำหนด',
year: 'ปี',
},
menu: {
@ -241,6 +242,16 @@ export default {
},
},
noti: {
title: 'การแจ้งเตือน',
caption: 'การแจ้งเตือนทั้งหมด',
unread: 'ยังไม่ได้อ่าน',
all: 'ทั้งหมด',
read: 'อ่าน',
viewAll: 'ดูทั้งหมด',
markAsRead: 'ทำเครื่องหมายว่าอ่านแล้ว',
},
form: {
tm6: {
transportation: 'เที่ยวบิน/พาหนะ',
@ -1119,6 +1130,7 @@ export default {
taskListNotFound: 'ไม่พบใบสั่งงาน',
creditNoteNotFound: 'ไม่พบใบลดหนี้',
debitNoteNotFound: 'ไม่พบใบเพิ่มหนี้',
notificationNotFound: 'ไม่พบการแจ้งเตือน',
productGroupIsUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
productIsUsed: 'สินค้าและบริการใช้งานอยู่',
@ -1320,11 +1332,14 @@ export default {
report: {
title: 'รายงาน',
customerName: 'ชื่อลูกค้า',
value: 'มูลค่า',
view: {
Document: 'รายงานสถานะเอกสาร',
Invoice: 'รายงานการชำระเงิน',
Product: 'รายงานสินค้าและบริการ',
Sale: 'รายงานสรุปยอดขาย',
Profit: 'รายงานกำไรและขาดทุน',
},
document: {
code: 'รหัส',
@ -1352,6 +1367,15 @@ export default {
name: 'ชื่อ',
count: 'จำนวนที่ขาย',
},
profit: {
byMonth: 'ตารางผลกำไรตามเดือน',
byYear: 'ตารางผลกำไรตามปี',
month: 'เดือน',
year: 'ปี (ค.ศ.)',
netProfit: 'กำไร',
expenses: 'ต้นทุน',
income: 'รายได้',
},
},
dashboard: {
title: 'Dashboard',
@ -1385,4 +1409,19 @@ export default {
caption: 'ตามใบกำกับภาษี',
},
},
month: {
1: 'มกราคม',
2: 'กุมภาพันธ์',
3: 'มีนาคม',
4: 'เมษายน',
5: 'พฤษภาคม',
6: 'มิถุนายน',
7: 'กรกฎาคม',
8: 'สิงหาคม',
9: 'กันยายน',
10: 'ตุลาคม',
11: 'พฤศจิกายน',
12: 'ธันวาคม',
},
};

View file

@ -1,10 +1,11 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed, reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
import { Icon } from '@iconify/vue';
import { useI18n } from 'vue-i18n';
import moment from 'moment';
import useLoader from 'stores/loader';
import ProfileMenu from './ProfileMenu.vue';
@ -17,6 +18,8 @@ import { useConfigStore } from 'src/stores/config';
import { useNavigator } from 'src/stores/navigator';
import { initLang, initTheme, Lang, setLang } from 'src/utils/ui';
import { baseUrl } from 'stores/utils';
import { useNotification } from 'src/stores/notification';
import NotiDialog from 'src/pages/00_notification/NotiDialog.vue';
const useMyBranch = useMyBranchStore();
const { fetchListMyBranch } = useMyBranch;
@ -27,18 +30,14 @@ interface NotificationButton {
active: boolean;
}
interface Notification {
id: string;
title: string;
content: string;
read: boolean;
}
const $q = useQuasar();
const loaderStore = useLoader();
const navigatorStore = useNavigator();
const notificationStore = useNotification();
const configStore = useConfigStore();
const { data: notificationData } = storeToRefs(notificationStore);
const { visible } = storeToRefs(loaderStore);
const { t } = useI18n({ useScope: 'global' });
const userStore = useUserStore();
@ -48,8 +47,9 @@ const canvasModal = ref(false);
const leftDrawerOpen = ref<boolean>(false);
const leftDrawerMini = ref(false);
const filterUnread = ref(false);
const unread = ref<number>(1);
const unread = computed<number>(
() => notificationData.value.filter((v) => !v.read).length || 0,
);
// const filterRole = ref<string[]>();
const userImage = ref<string>();
const userGender = ref('');
@ -65,33 +65,24 @@ const language: {
{ 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[]>([
{
item: 'ทั้งหมด',
item: 'all',
color: 'noti-switch-on',
active: true,
},
{
item: 'ยังไม่ได้อ่าน',
item: 'unread',
color: 'noti-switch-off',
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) {
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',
active: current.item === button.item,
}));
if (button.item === 'ยังไม่ได้อ่าน') {
if (button.item === 'unread') {
// noti.value?.result &&
filterUnread.value = true;
state.filterUnread = true;
}
if (button.item === 'ทั้งหมด') {
filterUnread.value = false;
if (button.item === 'all') {
state.filterUnread = false;
}
}
@ -123,12 +114,25 @@ function doLogout() {
});
}
function readNoti(id: string) {
state.notiDialog = true;
state.notiId = id;
}
onMounted(async () => {
initTheme();
initLang();
await configStore.getConfig();
{
const noti = await notificationStore.getNotificationList();
if (noti) {
notificationData.value = noti.result;
}
}
await fetchListMyBranch(getUserId() ?? '');
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">
<!-- notification -->
<!-- <q-btn
<q-btn
round
dense
flat
@ -286,7 +290,7 @@ onMounted(async () => {
:size="$q.screen.lt.sm ? 'sm' : ''"
:class="{ bordered: $q.dark.isActive, dark: $q.dark.isActive }"
style="color: var(--surface-1)"
@click="notiOpen = !notiOpen"
@click="state.notiOpen = !state.notiOpen"
>
<q-icon name="mdi-bell" />
<q-badge v-if="unread !== 0" rounded floating color="negative">
@ -297,10 +301,16 @@ onMounted(async () => {
:offset="[0, 10]"
anchor="bottom 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="text-subtitle1 text-weight-bold">แจงเตอน</div>
<div class="text-subtitle1 text-weight-bold">
{{ $t('noti.title') }}
</div>
<q-space />
</div>
<div class="q-px-md q-pb-md q-gutter-x-md">
@ -312,22 +322,30 @@ onMounted(async () => {
:flat="!btn.active"
:unelevated="btn.active"
:key="index"
:label="btn.item"
:label="$t('noti.' + btn.item)"
:class="btn.color"
@click="setActive(btn)"
/>
</div>
<q-infinite-scroll :offset="250">
<div class="caption cursor-pointer">
<div style="max-height: 30vh; width: 400px; overflow-y: auto">
<section
v-if="
state.filterUnread
? notificationData.filter((v) => !v.read).length
: notificationData.length
"
class="caption cursor-pointer"
>
<q-item
v-for="(item, i) in state.filterUnread
? notificationData.filter((v) => !v.read)
: notificationData"
dense
clickable
class="q-py-sm"
v-ripple
v-for="item in !filterUnread
? notification
: notification.filter((v) => !v.read)"
:key="item.id"
@click="readNoti(item.id)"
:key="i"
>
<q-avatar
color="positive"
@ -343,12 +361,11 @@ onMounted(async () => {
{{ item.title }}
</span>
<span class="block ellipsis full-width text-stone">
{{ item.content }}
{{ item.detail }}
</span>
</div>
<span align="right" class="col text-caption text-stone">
{{ moment(item.createdAt).fromNow() }}
5 s
</span>
<q-tooltip
anchor="top middle"
@ -356,21 +373,29 @@ onMounted(async () => {
:delay="1000"
:offset="[10, 10]"
>
{{ item.content }}
{{ item.detail }}
</q-tooltip>
</q-item>
</div>
<template v-slot:loading>
<div
class="text-center q-my-md"
v-if="noti && noti?.result.length < noti?.total"
>
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</section>
<section v-else class="text-center q-py-sm">
<span class="app-text-muted">
{{ $t('general.noData') }}
</span>
</section>
</div>
<div class="col bordered-t">
<q-btn
flat
dense
color="info"
class="full-width"
@click="() => $router.push('/notification')"
>
{{ $t('noti.viewAll') }}
</q-btn>
</div>
</q-menu>
</q-btn> -->
</q-btn>
<!-- เปลนนภาษา -->
<q-btn
@ -468,6 +493,8 @@ onMounted(async () => {
</template>
</DialogForm>
<NotiDialog v-model="state.notiDialog" v-model:id="state.notiId" />
<global-loading :visibility="visible" />
</q-layout>
</template>

View 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>

View 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>

View file

@ -641,6 +641,7 @@ async function storeDataLocal(id: string) {
:visible-columns="pageState.fieldSelected"
:grid="pageState.gridView"
:hide-edit="pageState.currentTab !== 'Issued'"
:hide-btn-preview="pageState.currentTab === 'PaymentSuccess'"
@preview="(id: any) => storeDataLocal(id)"
@view="
(item) => {
@ -667,6 +668,7 @@ async function storeDataLocal(id: string) {
class="col"
hide-kebab-delete
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
:hide-preview="pageState.currentTab === 'PaymentSuccess'"
:urgent="item.row.urgent"
:code="item.row.code"
:title="item.row.workName"

View file

@ -32,6 +32,8 @@ const prop = defineProps<{
isDebitNote?: boolean;
}>();
const firstCodePayment = defineModel<string>('firstCodePayment');
const refQFile = ref<InstanceType<typeof QFile>[]>([]);
const refQMenu = ref<InstanceType<typeof QMenu>[]>([]);
const paymentData = ref<QuotationPaymentData[]>([]);
@ -206,10 +208,15 @@ onMounted(async () => {
});
if (ret) {
paymentData.value = ret.result;
slipFile.value = paymentData.value.map((v) => ({
paymentId: v.id,
data: [],
}));
slipFile.value = paymentData.value.map((v, i) => {
if (i === 0) {
firstCodePayment.value = v.code;
}
return {
paymentId: v.id,
data: [],
};
});
}
});
</script>

View file

@ -196,7 +196,7 @@ const selectedWorkerItem = computed(() => {
];
});
const workerList = ref<Employee[]>([]);
const firstCodePayment = ref('');
const selectedProductGroup = ref('');
const selectedInstallmentNo = ref<number[]>([]);
const installmentAmount = ref<number>(0);
@ -1093,12 +1093,14 @@ watch(
// }
function storeDataLocal() {
quotationFormData.value.productServiceList = productServiceList.value;
quotationFormData.value.productServiceList = productService.value;
localStorage.setItem(
'quotation-preview',
JSON.stringify({
data: {
codeInvoice: code.value,
codePayment: firstCodePayment.value,
...quotationFormData.value,
productServiceList: productService.value,
},
@ -1939,6 +1941,7 @@ function covertToNode() {
view !== View.Complete
"
:data="quotationFormState.source"
v-model:first-code-payment="firstCodePayment"
@fetch-status="
() => {
fetchQuotation();
@ -2105,6 +2108,8 @@ function covertToNode() {
installmentAmount = props.row.amount;
view = View.Invoice;
console.log(code);
}
}
"
@ -2289,6 +2294,7 @@ function covertToNode() {
<MainButton
solid
icon="mdi-account-multiple-check-outline"
class="q-ml-sm"
color="207 96% 32%"
id="btn-select-invoice"
@click="

View file

@ -86,10 +86,22 @@ const summaryPrice = ref<SummaryPrice>({
finalPrice: 0,
});
async function fetchQuotationById(id: string) {
async function fetchQuotationById(id: string, codeInvoice: string) {
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) {
@ -190,7 +202,8 @@ onMounted(async () => {
if (data.value) {
if (!!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(
@ -203,8 +216,15 @@ onMounted(async () => {
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 = {
code: parsed?.meta?.source?.code ?? data.value?.code,
code: currentCode,
createdAt:
parsed?.meta?.source?.createdAt ??
new Date(data.value?.createdAt || ''),

View file

@ -88,12 +88,16 @@ function triggerView(opts: { quotationId: string }) {
window.open(url.toString(), '_blank');
}
function viewDocExample(quotationId: string) {
function viewDocExample(quotationId: string, codeInvoice: string) {
console.log(codeInvoice);
localStorage.setItem(
'quotation-preview',
JSON.stringify({
data: {
id: quotationId,
codeInvoice,
fetch: true,
},
}),
);
@ -346,7 +350,7 @@ watch(
:rows="data"
:grid="pageState.gridView"
@view="(quotationId) => triggerView({ quotationId })"
@preview="(quotationId) => viewDocExample(quotationId)"
@preview="(item) => viewDocExample(item.quotation.id, item.code)"
>
<template #grid="{ item }">
<div class="col-md-4 col-sm-6 col-12">
@ -401,9 +405,7 @@ watch(
() => triggerView({ quotationId: item.row.quotation.id })
"
@preview="
() => {
viewDocExample(item.row.quotation.id);
}
() => viewDocExample(item.row.quotation.id, item.row.code)
"
/>
</div>

View file

@ -41,7 +41,7 @@ withDefaults(
defineEmits<{
(e: 'view', id: string): void;
(e: 'preview', id: string): void;
(e: 'preview', data: Invoice): void;
(e: 'edit', data: T, index: number): void;
(e: 'delete', data: T, index: number): void;
(e: 'download', data: T, index: number): void;
@ -118,7 +118,7 @@ defineEmits<{
icon="mdi-play-box-outline"
size="12px"
:title="$t('preview.doc')"
@click.stop="$emit('preview', props.row.quotation.id)"
@click.stop="$emit('preview', props.row)"
/>
<q-btn

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
// NOTE: Library
import { computed, onMounted, reactive, watch } from 'vue';
import { ref, computed, onMounted, reactive, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { getRole } from 'src/services/keycloak';
@ -19,27 +19,58 @@ import {
colReport,
colReportProduct,
colReportSale,
colProfit,
colProfitByMoth,
colProfitByYear,
} from './constants';
import useFlowStore from 'src/stores/flow';
import { useReportStore } from 'src/stores/report';
import BadgeComponent from 'src/components/BadgeComponent.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
const navigatorStore = useNavigator();
const reportStore = useReportStore();
const flow = useFlowStore();
const dataReportProfitByYears = ref<ReportProfit['dataset']>([]);
const pastYears = ref<number>(5);
const optionsPastYears = ref<number[]>([5, 10, 20, 30, 50, 80]);
const {
dataReportQuotation,
dataReportInvoice,
dataReportReceipt,
dataReportSale,
dataReportProduct,
dataReportProfit,
} = storeToRefs(reportStore);
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() {
dataReportQuotation.value = (await reportStore.getReportQuotation()) || [];
}
@ -57,6 +88,16 @@ async function fetchReportProduct() {
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 () => {
navigatorStore.current.title = 'report.title';
navigatorStore.current.path = [{ text: '' }];
@ -64,7 +105,7 @@ onMounted(async () => {
const isExecutive = computed(() => {
const roles = userRoles.value;
return roles.includes('executive');
return roles.includes('executive') || roles.includes('system');
});
const filteredTabs = computed(() => {
@ -78,7 +119,7 @@ const filteredTabs = computed(() => {
});
const pageState = reactive({
currentTab: isExecutive.value ? ViewMode.Product : ViewMode.Document,
currentTab: ViewMode.Document,
});
async function fetchReportTab() {
@ -101,6 +142,10 @@ async function fetchReportTab() {
await fetchReportSale();
break;
}
case ViewMode.Profit: {
await fetchReportProfit();
break;
}
}
}
@ -111,6 +156,10 @@ onMounted(async () => {
watch([() => pageState.currentTab], async () => {
await fetchReportTab();
});
watch([() => pastYears.value], async () => {
await fetchReportProfitByYears(pastYears.value);
});
</script>
<template>
@ -166,11 +215,33 @@ watch([() => pageState.currentTab], async () => {
<!-- Quotatio -->
<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>
<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"
>
<template #status="{ item }">
@ -185,9 +256,34 @@ watch([() => pageState.currentTab], async () => {
<!-- Invoice -->
<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>
<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 }">
<BadgeComponent
:title="$t(`invoice.status.${item.row.status}`)"
@ -200,9 +296,34 @@ watch([() => pageState.currentTab], async () => {
<!-- Receipt -->
<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>
<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 }">
<BadgeComponent
:title="$t(`invoice.status.${item.row.status}`)"
@ -217,9 +338,34 @@ watch([() => pageState.currentTab], async () => {
<template v-if="pageState.currentTab === ViewMode.Invoice">
<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>
<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 }">
<BadgeComponent
:title="$t(`invoice.status.${item.row.status}`)"
@ -233,7 +379,17 @@ watch([() => pageState.currentTab], async () => {
<template v-if="pageState.currentTab === ViewMode.Product">
<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>
<TableReport
:row="dataReportProduct"
@ -245,7 +401,19 @@ watch([() => pageState.currentTab], async () => {
<template v-if="pageState.currentTab === ViewMode.Sale">
<div class="q-gutter-y-md">
<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>
<TableReport
:row="
@ -254,7 +422,9 @@ watch([() => pageState.currentTab], async () => {
code: v.code,
name:
v.customer.customerType === 'CORP'
? v.customerName
? $i18n.locale === 'eng'
? v.registerNameEN
: v.registerName
: $i18n.locale === 'eng'
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
@ -273,7 +443,17 @@ watch([() => pageState.currentTab], async () => {
</Expansion>
<Expansion default-opened>
<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 #main>
<TableReport
@ -295,7 +475,17 @@ watch([() => pageState.currentTab], async () => {
</template>
</Expansion>
<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>
<TableReport
:row="
@ -353,6 +543,99 @@ watch([() => pageState.currentTab], async () => {
</Expansion>
</div>
</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>
</div>
</section>

View file

@ -5,6 +5,8 @@ const prop = withDefaults(
defineProps<{
row: QTableProps['rows'];
columns: QTableProps['columns'];
hideHeader?: boolean;
hideBottom?: boolean;
}>(),
{
row: () => [],
@ -22,6 +24,8 @@ const prop = withDefaults(
}))
"
:columns
:hideHeader
:hideBottom
bordered
flat
selection="multiple"
@ -32,7 +36,12 @@ const prop = withDefaults(
style="background-color: hsla(var(--info-bg) / 0.07)"
: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) }}
</q-th>
</q-tr>

View file

@ -14,6 +14,7 @@ export enum ViewMode {
Receipt = 'receipt',
Product = 'product',
Sale = 'sale',
Profit = 'profit',
}
type ColumnsSale = {
@ -28,6 +29,25 @@ type ColumnsBySale = ColumnsSale & {
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 = [
{
name: 'code',
@ -41,6 +61,18 @@ export const colReportQuotation = [
label: 'report.document.status',
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',
align: 'center',
@ -68,6 +100,18 @@ export const colReport = [
label: 'report.table.status',
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',
align: 'center',
@ -145,9 +189,81 @@ export const colReportBySale = [
},
] 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 = [
{ label: 'Document', value: ViewMode.Document, by: ['user'] },
{ label: 'Invoice', value: ViewMode.Invoice, by: ['user'] },
{ label: 'Product', value: ViewMode.Product, by: ['user'] },
{ label: 'Sale', value: ViewMode.Sale, by: ['admin'] },
{ label: 'Profit', value: ViewMode.Profit, by: ['admin'] },
];

View file

@ -1,14 +1,15 @@
<script lang="ts" setup>
// NOTE: Library
import { onMounted, reactive } from 'vue';
import { onMounted, reactive, watch } from 'vue';
// NOTE: Components
import SelectInput from 'src/components/shared/SelectInput.vue';
import StatCardComponent from 'src/components/StatCardComponent.vue';
import ChartReceipt from './chart/ChartReceipt.vue';
import ChartOpportunity from './chart/ChartOpportunity.vue';
import ChartQuotationStatus from './chart/ChartQuotationStatus.vue';
import ChartSales from './chart/ChartSales.vue';
import VueDatePicker from '@vuepic/vue-datepicker';
// import ChartOpportunity from './chart/ChartOpportunity.vue';
// import ChartSales from './chart/ChartSales.vue';
// NOTE: Stores & Type
import { useNavigator } from 'src/stores/navigator';
@ -16,6 +17,7 @@ import { useQuotationStore } from 'src/stores/quotations';
import { storeToRefs } from 'pinia';
import { useReportStore } from 'src/stores/report';
import { PaymentDataStatus } from 'src/stores/payment/types';
import { dateFormatJS } from 'src/utils/datetime';
// NOTE: Variable
const navigatorStore = useNavigator();
@ -24,57 +26,101 @@ const reportStore = useReportStore();
const { stats: quotationStats } = storeToRefs(quotationStore);
const { dataReportPayment } = storeToRefs(reportStore);
const endDate = new Date();
const startDate = new Date(new Date().setFullYear(endDate.getFullYear() - 1));
const state = reactive({
role: 'admin',
year: 'now',
date: [startDate, endDate],
});
// NOTE: mock
const option = reactive({
role: [
{ label: 'ผู้ดูแล', value: 'admin' },
{ label: 'ผู้ใช้', value: 'user' },
],
year: [
{ label: 'ปีนี้', value: 'now' },
{ label: '2024', value: '2024' },
],
});
// const option = reactive({
// role: [
// { label: '', value: 'admin' },
// { label: '', value: 'user' },
// ],
// });
async function fetchData() {
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 () => {
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;
}
await fetchData();
});
watch(
() => state.date,
async () => {
await fetchData();
},
);
</script>
<template>
<div class="column full-height no-wrap">
<header class="row q-gutter-sm">
<SelectInput
<!-- <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"
for="selecte-role"
></SelectInput> -->
<VueDatePicker
utc
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>
<q-icon name="mdi-calendar-outline app-text-muted" />
<template #trigger>
<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>
</SelectInput>
</VueDatePicker>
<div class="col-12 scroll">
<div style="display: inline-block">

View file

@ -105,3 +105,9 @@ const chartOptions = computed(() => ({
></VueApexCharts>
</div>
</template>
<style scoped>
:deep(.apexcharts-menu.apexcharts-menu-open) {
background: var(--surface-1);
}
</style>

View file

@ -23,21 +23,6 @@ const prop = withDefaults(
},
);
const chartOptions = computed(() => {
const thaiMonths = [
'ม.ค.',
'ก.พ.',
'มี.ค.',
'เม.ย.',
'พ.ค.',
'มิ.ย.',
'ก.ค.',
'ส.ค.',
'ก.ย.',
'ต.ค.',
'พ.ย.',
'ธ.ค.',
];
return {
colors: ['#035aa1', '#ae3ec9', '#ffa94d', '#e64980'],
chart: {
@ -47,6 +32,9 @@ const chartOptions = computed(() => {
enabled: false,
},
},
tooltip: {
theme: 'dark',
},
legend: {
position: 'right',
},
@ -109,3 +97,9 @@ const detail = [
</div>
</div>
</template>
<style scoped>
:deep(.apexcharts-menu.apexcharts-menu-open) {
background: var(--surface-1);
}
</style>

View file

@ -135,6 +135,11 @@ const routes: RouteRecordRaw[] = [
name: 'dashBoard',
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',
component: () => import('pages/12_debit-note/FormPage.vue'),
},
{
path: '/debit-note/document-view',
name: 'DebitNoteDocumentView',

View file

@ -4,7 +4,13 @@ import { api } from 'src/boot/axios';
import { PaginationResult } from 'src/types';
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', () => {
const state = createDataRefBase<Notification>();
@ -28,9 +34,12 @@ export const useNotification = defineStore('noti-store', () => {
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 }>(
`/notification/${paymentId}`,
`/notification/${notificationId}`,
payload,
);
if (res.status >= 400) return null;
@ -45,6 +54,26 @@ export const useNotification = defineStore('noti-store', () => {
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 {
...state,
@ -52,5 +81,7 @@ export const useNotification = defineStore('noti-store', () => {
getNotificationList,
updateNotification,
deleteNotification,
deleteMultiNotification,
markReadNotification,
};
});

View file

@ -32,8 +32,11 @@ export const useQuotationStore = defineStore('quotation-store', () => {
canceled: 0,
});
async function getQuotationStats() {
const res = await api.get<QuotationStats>('/quotation/stats');
async function getQuotationStats(params?: {
startDate: string | Date;
endDate: string | Date;
}) {
const res = await api.get<QuotationStats>('/quotation/stats', { params });
if (res.status < 400) {
return res.data;
}

View file

@ -414,6 +414,7 @@ export type QuotationPaymentData = {
remark: string;
date: string;
id: string;
code: string;
};
export type Details = {

View file

@ -1,6 +1,5 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { Pagination, Status } from '../types';
import { api } from 'src/boot/axios';
import {
Report,
@ -8,10 +7,35 @@ import {
ReportProduct,
ReportQuotation,
ReportSale,
ReportProfit,
} from './types';
import { baseUrl } from '../utils';
import { getToken } from 'src/services/keycloak';
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() {
const res = await api.get<ReportQuotation[]>(`/${ENDPOINT}/quotation`);
if (res.status < 400) {
@ -21,6 +45,13 @@ export async function getReportQuotation() {
return null;
}
export async function downloadReportInvoice() {
await _download(
baseUrl + '/' + ENDPOINT + '/invoice/download',
'invoice-report',
);
}
export async function getReportInvoice() {
const res = await api.get<Report[]>(`/${ENDPOINT}/invoice`);
if (res.status < 400) {
@ -29,6 +60,13 @@ export async function getReportInvoice() {
return null;
}
export async function downloadReportReceipt() {
await _download(
baseUrl + '/' + ENDPOINT + '/receipt/download',
'receipt-report',
);
}
export async function getReportReceipt() {
const res = await api.get<Report[]>(`/${ENDPOINT}/receipt`);
if (res.status < 400) {
@ -37,6 +75,15 @@ export async function getReportReceipt() {
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() {
const res = await api.get<ReportSale>(`/${ENDPOINT}/sale`);
@ -46,16 +93,53 @@ export async function getReportSale() {
return null;
}
export async function downloadReportProduct() {
await _download(
baseUrl + '/' + ENDPOINT + '/receipt/download',
'product-report',
);
}
export async function getReportProduct() {
const res = await api.get<ReportProduct[]>(`/${ENDPOINT}/Product`);
const res = await api.get<ReportProduct[]>(`/${ENDPOINT}/product`);
if (res.status < 400) {
return res.data;
}
return null;
}
export async function getReportPayment() {
const res = await api.get<ReportPayment[]>(`/${ENDPOINT}/payment`);
export async function getReportPayment(params?: {
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) {
return res.data;
}
@ -69,6 +153,7 @@ export const useReportStore = defineStore('report-store', () => {
const dataReportSale = ref<ReportSale>();
const dataReportProduct = ref<ReportProduct[]>([]);
const dataReportPayment = ref<ReportPayment[]>([]);
const dataReportProfit = ref<ReportProfit>();
return {
dataReportQuotation,
@ -77,12 +162,19 @@ export const useReportStore = defineStore('report-store', () => {
dataReportSale,
dataReportProduct,
dataReportPayment,
dataReportProfit,
downloadReportQuotation,
getReportQuotation,
downloadReportInvoice,
getReportInvoice,
downloadReportReceipt,
getReportReceipt,
downloadReportSale,
getReportSale,
downloadReportProduct,
getReportProduct,
getReportPayment,
getReportProfit,
};
});

View file

@ -5,8 +5,10 @@ import { CustomerBranch } from '../customer';
import { PaymentDataStatus } from '../payment/types';
export type ReportQuotation = {
customerBranch: CustomerBranch;
updatedAt: Date | null;
createdAt: Date | null;
amount: number;
status: QuotationStatus;
code: string;
};
@ -20,7 +22,9 @@ export enum Status {
// use with Invoice and Receipt
export type Report = {
customerBranch: CustomerBranch;
createdAt: Date | null;
amount: number;
status: Status;
code: string;
};
@ -46,3 +50,16 @@ export type ReportSale = {
bySale: (User & { _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;
};

View file

@ -532,7 +532,7 @@ export function createDataRefBase<T>(
defaultPageSize = 30,
) {
return {
data: ref<T[]>(),
data: ref<T[]>([]),
page: ref<number>(defaultPage),
pageMax: ref<number>(defaultPageMax),
pageSize: ref<number>(defaultPageSize),