Compare commits

...

3 commits

Author SHA1 Message Date
Thanaphon Frappet
c750ed5fee feat: show profit
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 10s
2025-03-07 15:54:30 +07:00
Thanaphon Frappet
2a2613d72c refactor: add define props hide header and bottom 2025-03-07 15:54:30 +07:00
Thanaphon Frappet
7717523449 feat: add i18n of profit 2025-03-07 15:54:30 +07:00
7 changed files with 332 additions and 1 deletions

View file

@ -149,6 +149,7 @@ export default {
included: 'Included',
notIncluded: 'Not Included',
dueDate: 'Due date',
year: 'year',
},
menu: {
@ -1358,6 +1359,7 @@ export default {
Invoice: 'Payment Report',
Product: 'Product and Service Report',
Sale: 'Sales Summary Report',
Profit: 'Profit and Loss Report',
},
document: {
code: 'Code',
@ -1384,6 +1386,15 @@ export default {
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: {
title: 'Dashboard',
@ -1417,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: {
@ -1338,6 +1339,7 @@ export default {
Invoice: 'รายงานการชำระเงิน',
Product: 'รายงานสินค้าและบริการ',
Sale: 'รายงานสรุปยอดขาย',
Profit: 'รายงานกำไรและขาดทุน',
},
document: {
code: 'รหัส',
@ -1365,6 +1367,15 @@ export default {
name: 'ชื่อ',
count: 'จำนวนที่ขาย',
},
profit: {
byMonth: 'ตารางผลกำไรตามเดือน',
byYear: 'ตารางผลกำไรตามปี',
month: 'เดือน',
year: 'ปี (ค.ศ.)',
netProfit: 'กำไร',
expenses: 'ต้นทุน',
income: 'รายได้',
},
},
dashboard: {
title: 'Dashboard',
@ -1398,4 +1409,19 @@ export default {
caption: 'ตามใบกำกับภาษี',
},
},
month: {
1: 'มกราคม',
2: 'กุมภาพันธ์',
3: 'มีนาคม',
4: 'เมษายน',
5: 'พฤษภาคม',
6: 'มิถุนายน',
7: 'กรกฎาคม',
8: 'สิงหาคม',
9: 'กันยายน',
10: 'ตุลาคม',
11: 'พฤศจิกายน',
12: 'ธันวาคม',
},
};

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,28 +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()) || [];
}
@ -58,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: '' }];
@ -102,6 +142,10 @@ async function fetchReportTab() {
await fetchReportSale();
break;
}
case ViewMode.Profit: {
await fetchReportProfit();
break;
}
}
}
@ -112,6 +156,10 @@ onMounted(async () => {
watch([() => pageState.currentTab], async () => {
await fetchReportTab();
});
watch([() => pastYears.value], async () => {
await fetchReportProfitByYears(pastYears.value);
});
</script>
<template>
@ -495,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"

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',
@ -169,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

@ -7,6 +7,7 @@ import {
ReportProduct,
ReportQuotation,
ReportSale,
ReportProfit,
} from './types';
import { baseUrl } from '../utils';
import { getToken } from 'src/services/keycloak';
@ -120,6 +121,31 @@ export async function getReportPayment(params?: {
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); // ✅ 1 ม.ค. ของปีที่ต้องการ
opts.endDate = new Date(currentYear, 11, 31); // ✅ 31 ธ.ค. ของปีปัจจุบัน
}
const res = await api.get<ReportProfit>(`/${ENDPOINT}/profit`, {
params: {
startDate: opts.startDate,
endDate: opts.endDate,
},
});
if (res.status < 400) {
return res.data;
}
return null;
}
export const useReportStore = defineStore('report-store', () => {
const dataReportQuotation = ref<ReportQuotation[]>([]);
const dataReportInvoice = ref<Report[]>([]);
@ -127,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,
@ -135,6 +162,7 @@ export const useReportStore = defineStore('report-store', () => {
dataReportSale,
dataReportProduct,
dataReportPayment,
dataReportProfit,
downloadReportQuotation,
getReportQuotation,
@ -147,5 +175,6 @@ export const useReportStore = defineStore('report-store', () => {
downloadReportProduct,
getReportProduct,
getReportPayment,
getReportProfit,
};
});

View file

@ -50,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;
};