Compare commits

...

4 commits

Author SHA1 Message Date
Thanaphon Frappet
724f71ae4f refactor: import and bind value
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-03-05 16:40:47 +07:00
Thanaphon Frappet
7f14e6be46 feat: new Table and expansion 2025-03-05 16:39:20 +07:00
Thanaphon Frappet
ba10fc9609 feat: set constant variable 2025-03-05 16:37:53 +07:00
Thanaphon Frappet
2e32ad28d7 feat: add i18n 2025-03-05 16:36:54 +07:00
6 changed files with 667 additions and 4 deletions

View file

@ -0,0 +1,29 @@
<script lang="ts" setup>
defineProps<{
defaultOpened?: boolean;
}>();
</script>
<template>
<q-expansion-item
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="surface-1 q-py-sm text-medium text-body1"
:default-opened="defaultOpened"
>
<template #header>
<span>
<slot name="header" />
</span>
</template>
<main class="surface-1 q-pa-md full-width">
<slot name="main" />
</main>
</q-expansion-item>
</template>
<style scoped></style>

View file

@ -1336,4 +1336,44 @@ export default {
Succeed: 'Completed',
},
},
report: {
report: {
title: 'Report',
view: {
Document: 'Document Status Report',
Invoice: 'Payment Report',
Product: 'Product and Service Report',
admin: {
Product: 'Product Movement 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',
},
},
},
};

View file

@ -1315,4 +1315,43 @@ export default {
Succeed: 'เสร็จสิ้น',
},
},
report: {
title: 'รายงาน',
view: {
Document: 'รายงานสถานะเอกสาร',
Invoice: 'รายงานการชำระเงิน',
Product: 'รายงานสินค้าและบริการ',
admin: {
Product: 'รายงานการเคลื่อนไหวของสินค้า',
Sale: 'รายงานสรุปยอดขาย',
},
},
document: {
code: 'รหัส',
status: 'สถานะใบเสนอราคา',
createAt: 'วันที่สร้าง',
updateAt: 'วันที่แก้ไข',
},
table: {
code: 'รหัส',
status: 'สถานะ',
createAt: 'วันที่สร้าง',
},
product: {
did: 'จำนวนที่ออกไปดำเนินการ',
sale: 'จำนวนที่ขายออกไป',
name: 'ชื่อสินค้า',
code: 'รหัสสินค้า',
},
sale: {
byCustomer: 'ยอดขายตามสาขา',
byProductGroup: 'ยอดขายตามประเภทสินค้า',
bySale: 'ยอดขายตามพนักงานขาย',
code: 'รหัส',
name: 'ชื่อ',
count: 'จำนวนที่ขาย',
},
},
};

View file

@ -1,24 +1,362 @@
<script lang="ts" setup>
// NOTE: Library
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { computed, onMounted, reactive, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { getRole } from 'src/services/keycloak';
// NOTE: Components
import TableReport from './Table/TableReport.vue';
// NOTE: Stores & Type
import { baseUrl } from 'stores/utils';
import { hslaColors as colorsQuotation } from 'src/pages/05_quotation/constants';
import { hslaColors as colorsInvoice } from 'src/pages/10_invoice/constants';
import { useNavigator } from 'src/stores/navigator';
import {
ViewMode,
pageTabs,
colReportQuotation,
colReport,
colReportProduct,
colReportSale,
} 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';
// NOTE: Variable
const navigatorStore = useNavigator();
const reportStore = useReportStore();
const flow = useFlowStore();
const {
dataReportQuotation,
dataReportInvoice,
dataReportReceipt,
dataReportSale,
dataReportProduct,
} = storeToRefs(reportStore);
const userRoles = computed(() => getRole() || []);
async function fetchReportQuotation() {
dataReportQuotation.value = (await reportStore.getReportQuotation()) || [];
}
async function fetchReportInvoice() {
dataReportInvoice.value = (await reportStore.getReportInvoice()) || [];
}
async function fetchReportReceipt() {
dataReportReceipt.value = (await reportStore.getReportReceipt()) || [];
}
async function fetchReportSale() {
dataReportSale.value = (await reportStore.getReportSale()) || undefined;
}
async function fetchReportProduct() {
dataReportProduct.value = (await reportStore.getReportProduct()) || [];
}
onMounted(async () => {
navigatorStore.current.title = 'report.title';
navigatorStore.current.path = [{ text: '' }];
});
const isAdmin = computed(() => {
const roles = userRoles.value;
return roles.includes('head_of_admin') || roles.includes('head_of_account');
});
const filteredTabs = computed(() => {
return pageTabs.filter((tab) => {
if (isAdmin.value) {
return !(tab.by.length === 1 && tab.by.includes('user'));
} else {
return !(tab.by.length === 1 && tab.by.includes('admin'));
}
});
});
const pageState = reactive({
currentTab: isAdmin.value ? ViewMode.Product : ViewMode.Document,
});
async function fetchReportTab() {
switch (pageState.currentTab) {
case ViewMode.Document: {
await fetchReportQuotation();
await fetchReportInvoice();
await fetchReportReceipt();
break;
}
case ViewMode.Invoice: {
await fetchReportInvoice();
break;
}
case ViewMode.Product: {
await fetchReportProduct();
break;
}
case ViewMode.Sale: {
await fetchReportSale();
break;
}
}
}
onMounted(async () => {
await fetchReportTab();
});
watch([() => pageState.currentTab], async () => {
await fetchReportTab();
});
</script>
<template>
<!-- SEC: body content -->
<article class="col surface-2 flex items-center justify-center"></article>
<div class="column full-height no-wrap">
<section class="col surface-1 rounded bordered overflow-hidden">
<div class="column full-height">
<!-- SEC: header content -->
<header
class="row surface-3 justify-between full-width items-center"
style="z-index: 1"
>
<nav class="surface-2 bordered-b q-px-md full-width">
<q-tabs
inline-label
mobile-arrows
dense
v-model="pageState.currentTab"
align="left"
class="full-width"
active-color="info"
>
<q-tab
v-for="tab in filteredTabs"
:name="tab.value"
:key="tab.value"
@click="
() => {
pageState.currentTab = tab.value;
flow.rotate();
}
"
>
<div
class="row text-capitalize"
:class="
pageState.currentTab === tab.value
? 'text-bold'
: 'app-text-muted'
"
>
{{ $t(`report.view${isAdmin ? '.admin' : ''}.${tab.label}`) }}
</div>
</q-tab>
</q-tabs>
</nav>
</header>
<!-- SEC: body content -->
<article class="col surface-2 full-width scroll q-pa-md">
<!-- #TODO change Table -->
<template v-if="pageState.currentTab === ViewMode.Document">
<div class="q-gutter-y-md">
<!-- Quotatio -->
<Expansion default-opened>
<template #header>{{ $t('quotation.title') }}</template>
<template #main>
<TableReport
:row="dataReportQuotation"
:columns="colReportQuotation"
>
<template #status="{ item }">
<BadgeComponent
:title="$t(`quotation.status.${item.row.status}`)"
:hsla-color="colorsQuotation[item.row.status] || ''"
/>
</template>
</TableReport>
</template>
</Expansion>
<!-- Invoice -->
<Expansion default-opened>
<template #header>{{ $t('invoice.title') }}</template>
<template #main>
<TableReport :row="dataReportInvoice" :columns="colReport">
<template #status="{ item }">
<BadgeComponent
:title="$t(`invoice.status.${item.row.status}`)"
:hsla-color="colorsInvoice[item.row.status] || ''"
/>
</template>
</TableReport>
</template>
</Expansion>
<!-- Receipt -->
<Expansion default-opened>
<template #header>{{ $t('receipt.title') }}</template>
<template #main>
<TableReport :row="dataReportReceipt" :columns="colReport">
<template #status="{ item }">
<BadgeComponent
:title="$t(`invoice.status.${item.row.status}`)"
:hsla-color="colorsInvoice[item.row.status] || ''"
/>
</template>
</TableReport>
</template>
</Expansion>
</div>
</template>
<template v-if="pageState.currentTab === ViewMode.Invoice">
<Expansion default-opened>
<template #header>{{ $t('invoice.title') }}</template>
<template #main>
<TableReport :row="dataReportInvoice" :columns="colReport">
<template #status="{ item }">
<BadgeComponent
:title="$t(`invoice.status.${item.row.status}`)"
:hsla-color="colorsInvoice[item.row.status] || ''"
/>
</template>
</TableReport>
</template>
</Expansion>
</template>
<template v-if="pageState.currentTab === ViewMode.Product">
<Expansion default-opened>
<template #header>{{ $t('productService.title') }}</template>
<template #main>
<TableReport
:row="dataReportProduct"
:columns="colReportProduct"
/>
</template>
</Expansion>
</template>
<template v-if="pageState.currentTab === ViewMode.Sale">
<div class="q-gutter-y-md">
<Expansion default-opened>
<template #header>{{ $t('report.sale.byCustomer') }}</template>
<template #main>
<TableReport
:row="
dataReportSale?.byCustomer.map((v) => {
return {
code: v.code,
name:
v.customer.customerType === 'CORP'
? v.customerName
: $i18n.locale === 'eng'
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
_count: v._count,
};
})
"
:columns="colReportSale"
>
<template #name="{ item }">
{{ item.row.name }}
</template>
</TableReport>
</template>
</Expansion>
<Expansion default-opened>
<template #header>
{{ $t('report.sale.byProductGroup') }}
</template>
<template #main>
<TableReport
:row="
dataReportSale?.byProductGroup.map((v) => {
return {
code: v.code,
name: v.name,
_count: v._count,
};
})
"
:columns="colReportSale"
>
<template #name="{ item }">
{{ item.row.name }}
</template>
</TableReport>
</template>
</Expansion>
<Expansion default-opened>
<template #header>{{ $t('report.sale.bySale') }}</template>
<template #main>
<TableReport
:row="
dataReportSale?.bySale.map((v) => {
return {
code: v.code,
name:
$i18n.locale === 'eng'
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
_count: v._count,
url: v.selectedImage,
id: v.id,
gender: v.gender,
};
})
"
:columns="colReportSale"
>
<template #name="{ item }">
<q-avatar size="md">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/user/${item.row.id}/profile-image/${item.row.url}?ts=${Date.now()}`"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
:style="`${item.row.gender ? 'background: white' : 'background: linear-gradient(135deg,rgba(43, 137, 223, 1) 0%, rgba(230, 51, 81, 1) 100%);'}`"
>
<q-img
v-if="item.row.gender"
:src="
item.row.gender === 'male'
? '/no-img-man.png'
: '/no-img-female.png'
"
/>
<q-icon
v-else
size="sm"
name="mdi-account-outline"
style="color: white"
/>
</div>
</template>
</q-img>
</q-avatar>
{{ item.row.name }}
</template>
</TableReport>
</template>
</Expansion>
</div>
</template>
</article>
</div>
</section>
</div>
</template>
<style scoped></style>

View file

@ -0,0 +1,64 @@
<script setup lang="ts">
import { QTableSlots, QTableProps } from 'quasar';
const prop = withDefaults(
defineProps<{
row: QTableProps['rows'];
columns: QTableProps['columns'];
}>(),
{
row: () => [],
columns: () => [],
},
);
</script>
<template>
<q-table
:rows-per-page-options="[5, 0]"
:rows="
row.map((item, i) => ({
...item,
_index: i,
}))
"
:columns
bordered
flat
selection="multiple"
class="full-width"
>
<template v-slot:header="props">
<q-tr
style="background-color: hsla(var(--info-bg) / 0.07)"
:props="props"
>
<q-th v-for="col in columns" :key="col.name" :props="props">
{{ $t(col.label) }}
</q-th>
</q-tr>
</template>
<template
v-slot:body="props: {
row: any & { _index: number };
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
>
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
<q-td v-for="col in columns" :align="col.align">
<!-- NOTE: custom column will starts with # -->
<template v-if="!col.name.startsWith('#')">
{{
typeof col.field === 'string'
? props.row[col.field]
: col.field(props.row)
}}
</template>
<template v-else>
<slot :name="col.name.replace(/^#/, '')" :item="props" />
</template>
</q-td>
</q-tr>
</template>
</q-table>
</template>

View file

@ -0,0 +1,153 @@
import { QTableProps } from 'quasar';
import { Invoice, Receipt } from 'src/stores/payment/types';
import {
Report,
ReportProduct,
ReportQuotation,
} from 'src/stores/report/types';
import { formatNumberDecimal } from 'src/stores/utils';
import { dateFormatJS } from 'src/utils/datetime';
export enum ViewMode {
Document = 'document',
Invoice = 'invoice',
Receipt = 'receipt',
Product = 'product',
Sale = 'sale',
}
type ColumnsSale = {
code: string;
name: string;
_count: number;
};
type ColumnsBySale = ColumnsSale & {
url?: string;
id?: string;
gender?: string;
};
export const colReportQuotation = [
{
name: 'code',
align: 'center',
label: 'report.document.code',
field: (data: ReportQuotation) => data.code,
},
{
name: '#status',
align: 'center',
label: 'report.document.status',
field: '',
},
{
name: 'createAt',
align: 'center',
label: 'report.document.createAt',
field: (data: ReportQuotation) => dateFormatJS({ date: data.createdAt }),
},
{
name: 'updateAt',
align: 'center',
label: 'report.document.updateAt',
field: (data: ReportQuotation) => dateFormatJS({ date: data.updatedAt }),
},
] as const satisfies QTableProps['columns'];
export const colReport = [
{
name: 'code',
align: 'center',
label: 'report.table.code',
field: (data: Report) => data.code,
},
{
name: '#status',
align: 'center',
label: 'report.table.status',
field: '',
},
{
name: 'createAt',
align: 'center',
label: 'report.table.createAt',
field: (data: Report) => dateFormatJS({ date: data.createdAt }),
},
] as const satisfies QTableProps['columns'];
export const colReportProduct = [
{
name: 'code',
align: 'center',
label: 'report.product.code',
field: (data: ReportProduct) => data.code,
},
{
name: 'name',
align: 'center',
label: 'report.product.name',
field: (data: ReportProduct) => data.name,
},
{
name: 'sale',
align: 'center',
label: 'report.product.sale',
field: (data: ReportProduct) => data.sale,
},
{
name: 'did',
align: 'center',
label: 'report.product.did',
field: (data: ReportProduct) => data.did,
},
] as const satisfies QTableProps['columns'];
export const colReportSale = [
{
name: 'code',
align: 'left',
label: 'report.sale.code',
field: (data: ColumnsSale) => data.code,
},
{
name: '#name',
align: 'left',
label: 'report.sale.name',
field: '',
},
{
name: 'count',
align: 'center',
label: 'report.sale.count',
field: (data: ColumnsSale) => data._count,
},
] as const satisfies QTableProps['columns'];
export const colReportBySale = [
{
name: 'code',
align: 'left',
label: 'report.sale.code',
field: (data: ColumnsBySale) => data.code,
},
{
name: '#name',
align: 'left',
label: 'report.sale.name',
field: '',
},
{
name: 'count',
align: 'center',
label: 'report.sale.count',
field: (data: ColumnsBySale) => data._count,
},
] 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', 'admin'] },
{ label: 'Sale', value: ViewMode.Sale, by: ['admin'] },
];