feat: setup setting page
parent
29ff09d3aa
commit
4a08abe0b5
|
@ -2,4 +2,5 @@ VITE_APP_MODE=production
|
||||||
VITE_BASE_API_URL=http://103.187.147.241:30050/api
|
VITE_BASE_API_URL=http://103.187.147.241:30050/api
|
||||||
VITE_BASE_API_REPORT_URL=http://103.187.147.241:30050/api
|
VITE_BASE_API_REPORT_URL=http://103.187.147.241:30050/api
|
||||||
|
|
||||||
VITE_BASE_API_URL_LOCAL=https://api.office.weplayground.id/api
|
VITE_BASE_API_URL_LOCAL=https://api.office.weplayground.id/api
|
||||||
|
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -3,3 +3,4 @@ VITE_BASE_API_URL=https://api.sky.eigen.co.id/api
|
||||||
VITE_BASE_API_REPORT_URL=https://api.sky.eigen.co.id/api
|
VITE_BASE_API_REPORT_URL=https://api.sky.eigen.co.id/api
|
||||||
|
|
||||||
VITE_BASE_API_URL_LOCAL=https://api.sky.eigen.co.id/api
|
VITE_BASE_API_URL_LOCAL=https://api.sky.eigen.co.id/api
|
||||||
|
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -2,4 +2,5 @@ VITE_APP_MODE=production
|
||||||
VITE_BASE_API_URL=http://172.16.2.101:30050/api
|
VITE_BASE_API_URL=http://172.16.2.101:30050/api
|
||||||
VITE_BASE_API_REPORT_URL=http://172.16.2.101:30050/api
|
VITE_BASE_API_REPORT_URL=http://172.16.2.101:30050/api
|
||||||
|
|
||||||
VITE_BASE_API_URL_LOCAL=http://172.16.2.101:30050/api
|
VITE_BASE_API_URL_LOCAL=http://172.16.2.101:30050/api
|
||||||
|
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -2,4 +2,5 @@ VITE_APP_MODE=production
|
||||||
VITE_BASE_API_URL=http://103.187.147.241:30050/api
|
VITE_BASE_API_URL=http://103.187.147.241:30050/api
|
||||||
VITE_BASE_API_REPORT_URL=http://103.187.147.241:30050/api
|
VITE_BASE_API_REPORT_URL=http://103.187.147.241:30050/api
|
||||||
|
|
||||||
VITE_BASE_API_URL_LOCAL=http://103.187.147.241:30050/api
|
VITE_BASE_API_URL_LOCAL=http://103.187.147.241:30050/api
|
||||||
|
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -1,277 +1,19 @@
|
||||||
import axios from 'axios';
|
import { lazy } from 'react';
|
||||||
import AdminLayout from './layout';
|
import AdminLayout from './layout';
|
||||||
import { API_URL, currencyFormatter } from '@pos/base';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Card, Col, DatePicker, notification, Row, Table } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import lodash from 'lodash';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
export default function Admin() {
|
const ReportModule = lazy(() => import('./pages/report'));
|
||||||
const [dataItem, setDataItem] = useState<any[]>([]);
|
const SettingModule = lazy(() => import('./pages/setting'));
|
||||||
const [dataItemKeys, setDataItemKeys] = useState<any[]>([]);
|
|
||||||
const [loadingDataItem, setLoadingDataItem] = useState<boolean>(false);
|
|
||||||
const [dataItemTotalPax, setDataItemTotalPax] = useState<number>(0);
|
|
||||||
const [dataItemTotalRevenue, setDataItemTotalRevenue] = useState<number>(0);
|
|
||||||
|
|
||||||
const [dataItemMaster, setDataItemMaster] = useState<any[]>([]);
|
|
||||||
const [dataItemMasterKeys, setDataItemMasterKeys] = useState<any[]>([]);
|
|
||||||
const [loadingDataItemMaster, setLoadingDataItemMaster] = useState<boolean>(false);
|
|
||||||
const [dataItemMasterTotalPax, setDataItemMasterTotalPax] = useState<number>(0);
|
|
||||||
const [dataItemMasterTotalRevenue, setDataItemMasterTotalRevenue] = useState<number>(0);
|
|
||||||
|
|
||||||
const [filterDate, setFilerDate] = useState(dayjs());
|
|
||||||
|
|
||||||
async function getDataItem(params: any) {
|
|
||||||
setLoadingDataItem(true);
|
|
||||||
await axios
|
|
||||||
.get(API_URL.REPORT_SUMMARY_INCOME_ITEM, { params: params })
|
|
||||||
.then((resp) => {
|
|
||||||
const data = resp.data.data;
|
|
||||||
|
|
||||||
const groupedData = lodash(data)
|
|
||||||
.groupBy('item_owner') // Group by item_owner
|
|
||||||
.map((items, owner) => ({
|
|
||||||
// Map over each group to sum values and keep children
|
|
||||||
title: owner,
|
|
||||||
tr_item__qty: lodash.sumBy(items, (item) => Number(item.tr_item__qty)), // Convert to number
|
|
||||||
tr_item__total_net_price: lodash.sumBy(items, (item) => Number(item.tr_item__total_net_price)), // Convert to number
|
|
||||||
children: items.map((item) => {
|
|
||||||
return { ...item, title: item.tr_item__item_name };
|
|
||||||
}), // Include the original data as children
|
|
||||||
}))
|
|
||||||
.value()
|
|
||||||
.map((item) => {
|
|
||||||
return {
|
|
||||||
key: v4(),
|
|
||||||
...item,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPax = lodash.sumBy(data, (item: any) => Number(item.tr_item__qty));
|
|
||||||
const totalRevenue = lodash.sumBy(data, (item: any) => Number(item.tr_item__total_net_price));
|
|
||||||
setDataItemTotalPax(totalPax);
|
|
||||||
setDataItemTotalRevenue(totalRevenue);
|
|
||||||
|
|
||||||
setDataItemKeys(groupedData.map((item) => item.key));
|
|
||||||
setDataItem(groupedData);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
notification.error({ message: err?.message });
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoadingDataItem(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDataItemMaster(params: any) {
|
|
||||||
setLoadingDataItemMaster(true);
|
|
||||||
await axios
|
|
||||||
.get(API_URL.REPORT_SUMMARY_INCOME_ITEM_MASTER, { params: params })
|
|
||||||
.then((resp) => {
|
|
||||||
const data = resp.data.data;
|
|
||||||
const groupedData = lodash(data)
|
|
||||||
.groupBy('item_owner') // Group by item_owner
|
|
||||||
.map((items, owner) => ({
|
|
||||||
// Map over each group to sum values and keep children
|
|
||||||
title: owner,
|
|
||||||
tr_item__qty: lodash.sumBy(items, (item) => Number(item.tr_item__qty)), // Convert to number
|
|
||||||
tr_item_bundling__total_net_price: lodash.sumBy(items, (item) =>
|
|
||||||
Number(item.tr_item_bundling__total_net_price),
|
|
||||||
), // Convert to number
|
|
||||||
children: items.map((item) => {
|
|
||||||
let title = '';
|
|
||||||
if (item.tr_item_bundling__item_name) {
|
|
||||||
title = `${item.tr_item_bundling__item_name} / ${item.tr_item__item_name}`;
|
|
||||||
} else {
|
|
||||||
title = item.tr_item__item_name;
|
|
||||||
}
|
|
||||||
return { ...item, title: title };
|
|
||||||
}), // Include the original data as children
|
|
||||||
}))
|
|
||||||
.value()
|
|
||||||
.map((item) => {
|
|
||||||
return {
|
|
||||||
key: v4(),
|
|
||||||
...item,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPax = lodash.sumBy(data, (item: any) => Number(item.tr_item__qty));
|
|
||||||
const totalRevenue = lodash.sumBy(data, (item: any) => Number(item.tr_item_bundling__total_net_price));
|
|
||||||
setDataItemMasterTotalPax(totalPax);
|
|
||||||
setDataItemMasterTotalRevenue(totalRevenue);
|
|
||||||
|
|
||||||
setDataItemMasterKeys(groupedData.map((item) => item.key));
|
|
||||||
setDataItemMaster(groupedData);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
notification.error({ message: err?.message });
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoadingDataItemMaster(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGetDate(date: any) {
|
|
||||||
getDataItem({ date });
|
|
||||||
getDataItemMaster({ date });
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (filterDate) {
|
|
||||||
handleGetDate(filterDate.format('DD-MM-YYYY'));
|
|
||||||
}
|
|
||||||
}, [filterDate]);
|
|
||||||
|
|
||||||
|
export default function AppModule() {
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<Row>
|
<Routes>
|
||||||
<Col xl={8} lg={8} md={12} span={24}>
|
<Route path="/report" element={<ReportModule />} />
|
||||||
<DatePicker
|
<Route path="/setting" element={<SettingModule />} />
|
||||||
size="large"
|
<Route path="/" element={<Navigate to="/app/report" />} />
|
||||||
popupStyle={{ fontSize: 16 }}
|
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||||
allowClear={false}
|
</Routes>
|
||||||
value={filterDate}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
format={'DD-MM-YYYY'}
|
|
||||||
onChange={setFilerDate}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div style={{ marginBottom: 20 }}></div>
|
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
<Col xl={12} lg={12} span={24}>
|
|
||||||
<Card
|
|
||||||
title={
|
|
||||||
<Row style={{ paddingTop: 10, paddingBottom: 10 }}>
|
|
||||||
<Col span={24}>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 16, fontWeight: 600 }}
|
|
||||||
>{`Pendapatan Per Item ${filterDate.format('DD-MM-YYYY')}`}</div>
|
|
||||||
</Col>
|
|
||||||
<Col xl={20} lg={20} span={24}>
|
|
||||||
<div
|
|
||||||
style={{ fontWeight: 400, fontSize: 12, color: 'grey', textWrap: 'wrap' }}
|
|
||||||
>{`Total revenue mungkin berbeda dengan pendapatan per item master disebabkan pengambilan harga kepada harga bundling.`}</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Row gutter={[8, 8]}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL PAX</div>
|
|
||||||
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>{dataItemTotalPax}</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL REVENUE</div>
|
|
||||||
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>
|
|
||||||
{currencyFormatter({ value: dataItemTotalRevenue })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div style={{ marginBottom: 10 }}></div>
|
|
||||||
<Table
|
|
||||||
bordered
|
|
||||||
size="small"
|
|
||||||
dataSource={dataItem}
|
|
||||||
pagination={false}
|
|
||||||
loading={loadingDataItem}
|
|
||||||
scroll={{ x: 'max-width', y: 350 }}
|
|
||||||
rowKey={(child) => child.key} // Make sure each child row has a unique key
|
|
||||||
expandable={{ expandedRowKeys: dataItemKeys, showExpandColumn: false }}
|
|
||||||
rowClassName={(row) => (row.key ? 'row-group' : '')}
|
|
||||||
rowHoverable={false}
|
|
||||||
columns={[
|
|
||||||
{ key: 'title', dataIndex: 'title', title: 'TITLE', width: 170 },
|
|
||||||
{ key: 'tr_item__qty', dataIndex: 'tr_item__qty', title: 'PAX', width: 70 },
|
|
||||||
{
|
|
||||||
key: 'tr_item__total_net_price',
|
|
||||||
dataIndex: 'tr_item__total_net_price',
|
|
||||||
title: 'REVENUE',
|
|
||||||
width: 120,
|
|
||||||
render: (value) => currencyFormatter({ value }),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xl={12} lg={12} span={24}>
|
|
||||||
<Card
|
|
||||||
title={
|
|
||||||
<Row style={{ paddingTop: 10, paddingBottom: 10 }}>
|
|
||||||
<Col span={24}>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 16, fontWeight: 600 }}
|
|
||||||
>{`Pendapatan Per Item Master ${filterDate.format('DD-MM-YYYY')}`}</div>
|
|
||||||
</Col>
|
|
||||||
<Col xl={20} lg={20} span={24}>
|
|
||||||
<div
|
|
||||||
style={{ fontWeight: 400, fontSize: 12, color: 'grey', textWrap: 'wrap' }}
|
|
||||||
>{`Total revenue mungkin berbeda dengan pendapatan per item disebabkan harga item master mengambil harga jual standard item.`}</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Row gutter={[8, 8]}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL PAX</div>
|
|
||||||
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>
|
|
||||||
{dataItemMasterTotalPax}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL REVENUE</div>
|
|
||||||
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>
|
|
||||||
{currencyFormatter({ value: dataItemMasterTotalRevenue })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div style={{ marginBottom: 10 }}></div>
|
|
||||||
<Table
|
|
||||||
bordered
|
|
||||||
size="small"
|
|
||||||
dataSource={dataItemMaster}
|
|
||||||
pagination={false}
|
|
||||||
loading={loadingDataItemMaster}
|
|
||||||
scroll={{ x: 'max-width', y: 350 }}
|
|
||||||
rowKey={(child) => child.key} // Make sure each child row has a unique key
|
|
||||||
expandable={{ expandedRowKeys: dataItemMasterKeys, showExpandColumn: false }}
|
|
||||||
rowClassName={(row) => (row.key ? 'row-group' : '')}
|
|
||||||
rowHoverable={false}
|
|
||||||
columns={[
|
|
||||||
{ key: 'title', dataIndex: 'title', title: 'TITLE', width: 170 },
|
|
||||||
|
|
||||||
{ key: 'tr_item__qty', dataIndex: 'tr_item__qty', title: 'PAX', width: 70 },
|
|
||||||
{
|
|
||||||
key: 'tr_item_bundling__total_net_price',
|
|
||||||
dataIndex: 'tr_item_bundling__total_net_price',
|
|
||||||
title: 'REVENUE',
|
|
||||||
width: 120,
|
|
||||||
render: (value) => currencyFormatter({ value }),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { App, Avatar, Dropdown, Flex, Image, Layout } from 'antd';
|
import { App, Avatar, Dropdown, Flex, Image, Layout } from 'antd';
|
||||||
import { ReactNode, useState } from 'react';
|
import { ReactNode, useState } from 'react';
|
||||||
import { Content, Header } from 'antd/es/layout/layout';
|
import { Content, Header } from 'antd/es/layout/layout';
|
||||||
import { API_URL, getInitialName, handleLogout, UserDataState } from '@pos/base';
|
import { ACCESS_SETTING, API_URL, getInitialName, handleLogout, UserDataState } from '@pos/base';
|
||||||
import { FaUser } from 'react-icons/fa';
|
import { FaUser } from 'react-icons/fa';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Logo from '../../../base/presentation/assets/images/we-logo.png';
|
import Logo from '../../../base/presentation/assets/images/we-logo.png';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import LocalDataConfiguration from './components/local-data-configuration';
|
import LocalDataConfiguration from './components/local-data-configuration';
|
||||||
import { LogoutOutlined, SettingOutlined } from '@ant-design/icons';
|
import { FileTextOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface AdminLayoutProps {
|
interface AdminLayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminLayout(props: AdminLayoutProps) {
|
export default function AdminLayout(props: AdminLayoutProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { modal } = App.useApp();
|
const { modal } = App.useApp();
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
@ -22,7 +24,7 @@ export default function AdminLayout(props: AdminLayoutProps) {
|
||||||
|
|
||||||
const [openModalConfig, setOpenModalConfig] = useState(false);
|
const [openModalConfig, setOpenModalConfig] = useState(false);
|
||||||
const onCancelModalConfig = () => setOpenModalConfig(false);
|
const onCancelModalConfig = () => setOpenModalConfig(false);
|
||||||
const onOpenModalConfig = () => setOpenModalConfig(true);
|
// const onOpenModalConfig = () => setOpenModalConfig(true);
|
||||||
|
|
||||||
async function handleClickLogout() {
|
async function handleClickLogout() {
|
||||||
try {
|
try {
|
||||||
|
@ -35,11 +37,22 @@ export default function AdminLayout(props: AdminLayoutProps) {
|
||||||
|
|
||||||
function checkAllowAccessSetting() {
|
function checkAllowAccessSetting() {
|
||||||
const username = user?.username;
|
const username = user?.username;
|
||||||
const allowList = ['Endy', 'superadmin'];
|
const accessSetting = ACCESS_SETTING ?? '';
|
||||||
|
|
||||||
|
const allowList = accessSetting.split('|');
|
||||||
if (allowList.includes(username)) return true;
|
if (allowList.includes(username)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gotoHome() {
|
||||||
|
navigate('/app');
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoSetting() {
|
||||||
|
// onOpenModalConfig();
|
||||||
|
navigate('/app/setting');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Header
|
<Header
|
||||||
|
@ -56,7 +69,7 @@ export default function AdminLayout(props: AdminLayoutProps) {
|
||||||
>
|
>
|
||||||
<Flex style={{ width: '100%' }} align="center">
|
<Flex style={{ width: '100%' }} align="center">
|
||||||
<Flex>
|
<Flex>
|
||||||
<Image src={Logo} width={50} preview={false} />
|
<Image src={Logo} width={50} preview={false} onClick={gotoHome} style={{ cursor: 'pointer' }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex style={{ marginLeft: 'auto' }} gap={15} align="center">
|
<Flex style={{ marginLeft: 'auto' }} gap={15} align="center">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -66,15 +79,26 @@ export default function AdminLayout(props: AdminLayoutProps) {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: 'Settings',
|
label: 'Report',
|
||||||
icon: <SettingOutlined />,
|
icon: <FileTextOutlined />,
|
||||||
onClick: () => onOpenModalConfig(),
|
onClick: () => gotoHome(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
|
key: 'divider_1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
|
label: 'Settings',
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
onClick: () => gotoSetting(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'divider_2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
label: 'Logout',
|
label: 'Logout',
|
||||||
icon: <LogoutOutlined />,
|
icon: <LogoutOutlined />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
@ -91,7 +115,7 @@ export default function AdminLayout(props: AdminLayoutProps) {
|
||||||
]
|
]
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const isAllowSetting = checkAllowAccessSetting();
|
const isAllowSetting = checkAllowAccessSetting();
|
||||||
if (!isAllowSetting && (item.key === '1' || item.type === 'divider')) {
|
if (!isAllowSetting && ['1', '2', 'divider_1', 'divider_2'].includes(item.key)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL, currencyFormatter } from '@pos/base';
|
||||||
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
|
import { Card, Col, DatePicker, notification, Row, Table } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export default function ReportModule() {
|
||||||
|
const [dataItem, setDataItem] = useState<any[]>([]);
|
||||||
|
const [dataItemKeys, setDataItemKeys] = useState<any[]>([]);
|
||||||
|
const [loadingDataItem, setLoadingDataItem] = useState<boolean>(false);
|
||||||
|
const [dataItemTotalPax, setDataItemTotalPax] = useState<number>(0);
|
||||||
|
const [dataItemTotalRevenue, setDataItemTotalRevenue] = useState<number>(0);
|
||||||
|
|
||||||
|
const [dataItemMaster, setDataItemMaster] = useState<any[]>([]);
|
||||||
|
const [dataItemMasterKeys, setDataItemMasterKeys] = useState<any[]>([]);
|
||||||
|
const [loadingDataItemMaster, setLoadingDataItemMaster] = useState<boolean>(false);
|
||||||
|
const [dataItemMasterTotalPax, setDataItemMasterTotalPax] = useState<number>(0);
|
||||||
|
const [dataItemMasterTotalRevenue, setDataItemMasterTotalRevenue] = useState<number>(0);
|
||||||
|
|
||||||
|
const [filterDate, setFilerDate] = useState(dayjs());
|
||||||
|
|
||||||
|
async function getDataItem(params: any) {
|
||||||
|
setLoadingDataItem(true);
|
||||||
|
await axios
|
||||||
|
.get(API_URL.REPORT_SUMMARY_INCOME_ITEM, { params: params })
|
||||||
|
.then((resp) => {
|
||||||
|
const data = resp.data.data;
|
||||||
|
|
||||||
|
const groupedData = lodash(data)
|
||||||
|
.groupBy('item_owner') // Group by item_owner
|
||||||
|
.map((items, owner) => ({
|
||||||
|
// Map over each group to sum values and keep children
|
||||||
|
title: owner,
|
||||||
|
tr_item__qty: lodash.sumBy(items, (item) => Number(item.tr_item__qty)), // Convert to number
|
||||||
|
tr_item__total_net_price: lodash.sumBy(items, (item) => Number(item.tr_item__total_net_price)), // Convert to number
|
||||||
|
children: items.map((item) => {
|
||||||
|
return { ...item, title: item.tr_item__item_name };
|
||||||
|
}), // Include the original data as children
|
||||||
|
}))
|
||||||
|
.value()
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
key: v4(),
|
||||||
|
...item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPax = lodash.sumBy(data, (item: any) => Number(item.tr_item__qty));
|
||||||
|
const totalRevenue = lodash.sumBy(data, (item: any) => Number(item.tr_item__total_net_price));
|
||||||
|
setDataItemTotalPax(totalPax);
|
||||||
|
setDataItemTotalRevenue(totalRevenue);
|
||||||
|
|
||||||
|
setDataItemKeys(groupedData.map((item) => item.key));
|
||||||
|
setDataItem(groupedData);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notification.error({ message: err?.message });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoadingDataItem(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDataItemMaster(params: any) {
|
||||||
|
setLoadingDataItemMaster(true);
|
||||||
|
await axios
|
||||||
|
.get(API_URL.REPORT_SUMMARY_INCOME_ITEM_MASTER, { params: params })
|
||||||
|
.then((resp) => {
|
||||||
|
const data = resp.data.data;
|
||||||
|
const groupedData = lodash(data)
|
||||||
|
.groupBy('item_owner') // Group by item_owner
|
||||||
|
.map((items, owner) => ({
|
||||||
|
// Map over each group to sum values and keep children
|
||||||
|
title: owner,
|
||||||
|
tr_item__qty: lodash.sumBy(items, (item) => Number(item.tr_item__qty)), // Convert to number
|
||||||
|
tr_item_bundling__total_net_price: lodash.sumBy(items, (item) =>
|
||||||
|
Number(item.tr_item_bundling__total_net_price),
|
||||||
|
), // Convert to number
|
||||||
|
children: items.map((item) => {
|
||||||
|
let title = '';
|
||||||
|
if (item.tr_item_bundling__item_name) {
|
||||||
|
title = `${item.tr_item_bundling__item_name} / ${item.tr_item__item_name}`;
|
||||||
|
} else {
|
||||||
|
title = item.tr_item__item_name;
|
||||||
|
}
|
||||||
|
return { ...item, title: title };
|
||||||
|
}), // Include the original data as children
|
||||||
|
}))
|
||||||
|
.value()
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
key: v4(),
|
||||||
|
...item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPax = lodash.sumBy(data, (item: any) => Number(item.tr_item__qty));
|
||||||
|
const totalRevenue = lodash.sumBy(data, (item: any) => Number(item.tr_item_bundling__total_net_price));
|
||||||
|
setDataItemMasterTotalPax(totalPax);
|
||||||
|
setDataItemMasterTotalRevenue(totalRevenue);
|
||||||
|
|
||||||
|
setDataItemMasterKeys(groupedData.map((item) => item.key));
|
||||||
|
setDataItemMaster(groupedData);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notification.error({ message: err?.message });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoadingDataItemMaster(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGetDate(date: any) {
|
||||||
|
getDataItem({ date });
|
||||||
|
getDataItemMaster({ date });
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filterDate) {
|
||||||
|
handleGetDate(filterDate.format('DD-MM-YYYY'));
|
||||||
|
}
|
||||||
|
}, [filterDate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Row>
|
||||||
|
<Col xl={8} lg={8} md={12} span={24}>
|
||||||
|
<DatePicker
|
||||||
|
size="large"
|
||||||
|
popupStyle={{ fontSize: 16 }}
|
||||||
|
allowClear={false}
|
||||||
|
value={filterDate}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
format={'DD-MM-YYYY'}
|
||||||
|
onChange={setFilerDate}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div style={{ marginBottom: 20 }}></div>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col xl={12} lg={12} span={24}>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Row style={{ paddingTop: 10, paddingBottom: 10 }}>
|
||||||
|
<Col span={24}>
|
||||||
|
<div
|
||||||
|
style={{ fontSize: 16, fontWeight: 600 }}
|
||||||
|
>{`Pendapatan Per Item ${filterDate.format('DD-MM-YYYY')}`}</div>
|
||||||
|
</Col>
|
||||||
|
<Col xl={20} lg={20} span={24}>
|
||||||
|
<div
|
||||||
|
style={{ fontWeight: 400, fontSize: 12, color: 'grey', textWrap: 'wrap' }}
|
||||||
|
>{`Total revenue mungkin berbeda dengan pendapatan per item master disebabkan pengambilan harga kepada harga bundling.`}</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Row gutter={[8, 8]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL PAX</div>
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>{dataItemTotalPax}</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL REVENUE</div>
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>
|
||||||
|
{currencyFormatter({ value: dataItemTotalRevenue })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div style={{ marginBottom: 10 }}></div>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
dataSource={dataItem}
|
||||||
|
pagination={false}
|
||||||
|
loading={loadingDataItem}
|
||||||
|
scroll={{ x: 'max-width', y: 350 }}
|
||||||
|
rowKey={(child) => child.key} // Make sure each child row has a unique key
|
||||||
|
expandable={{ expandedRowKeys: dataItemKeys, showExpandColumn: false }}
|
||||||
|
rowClassName={(row) => (row.key ? 'row-group' : '')}
|
||||||
|
rowHoverable={false}
|
||||||
|
columns={[
|
||||||
|
{ key: 'title', dataIndex: 'title', title: 'TITLE', width: 170 },
|
||||||
|
{ key: 'tr_item__qty', dataIndex: 'tr_item__qty', title: 'PAX', width: 70 },
|
||||||
|
{
|
||||||
|
key: 'tr_item__total_net_price',
|
||||||
|
dataIndex: 'tr_item__total_net_price',
|
||||||
|
title: 'REVENUE',
|
||||||
|
width: 120,
|
||||||
|
render: (value) => currencyFormatter({ value }),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xl={12} lg={12} span={24}>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Row style={{ paddingTop: 10, paddingBottom: 10 }}>
|
||||||
|
<Col span={24}>
|
||||||
|
<div
|
||||||
|
style={{ fontSize: 16, fontWeight: 600 }}
|
||||||
|
>{`Pendapatan Per Item Master ${filterDate.format('DD-MM-YYYY')}`}</div>
|
||||||
|
</Col>
|
||||||
|
<Col xl={20} lg={20} span={24}>
|
||||||
|
<div
|
||||||
|
style={{ fontWeight: 400, fontSize: 12, color: 'grey', textWrap: 'wrap' }}
|
||||||
|
>{`Total revenue mungkin berbeda dengan pendapatan per item disebabkan harga item master mengambil harga jual standard item.`}</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Row gutter={[8, 8]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL PAX</div>
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>
|
||||||
|
{dataItemMasterTotalPax}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Card styles={{ body: { padding: '6px 12px 6px 12px' } }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>TOTAL REVENUE</div>
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}>
|
||||||
|
{currencyFormatter({ value: dataItemMasterTotalRevenue })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div style={{ marginBottom: 10 }}></div>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
dataSource={dataItemMaster}
|
||||||
|
pagination={false}
|
||||||
|
loading={loadingDataItemMaster}
|
||||||
|
scroll={{ x: 'max-width', y: 350 }}
|
||||||
|
rowKey={(child) => child.key} // Make sure each child row has a unique key
|
||||||
|
expandable={{ expandedRowKeys: dataItemMasterKeys, showExpandColumn: false }}
|
||||||
|
rowClassName={(row) => (row.key ? 'row-group' : '')}
|
||||||
|
rowHoverable={false}
|
||||||
|
columns={[
|
||||||
|
{ key: 'title', dataIndex: 'title', title: 'TITLE', width: 170 },
|
||||||
|
|
||||||
|
{ key: 'tr_item__qty', dataIndex: 'tr_item__qty', title: 'PAX', width: 70 },
|
||||||
|
{
|
||||||
|
key: 'tr_item_bundling__total_net_price',
|
||||||
|
dataIndex: 'tr_item_bundling__total_net_price',
|
||||||
|
title: 'REVENUE',
|
||||||
|
width: 120,
|
||||||
|
render: (value) => currencyFormatter({ value }),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { ACCESS_SETTING, UserDataState } from '@pos/base';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
export default function SettingModule() {
|
||||||
|
const user = useRecoilValue(UserDataState);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function checkAllowAccessSetting() {
|
||||||
|
const username = user?.username;
|
||||||
|
const accessSetting = ACCESS_SETTING ?? '';
|
||||||
|
|
||||||
|
const allowList = accessSetting.split('|');
|
||||||
|
if (allowList.includes(username)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!checkAllowAccessSetting()) {
|
||||||
|
navigate('/app');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div>Setting</div>;
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import { APP_THEME } from '@pos/base/presentation/assets/themes';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const AuthApp = lazy(() => import('./auth'));
|
const AuthApp = lazy(() => import('./auth'));
|
||||||
const PrivateApp = lazy(() => import('./admin'));
|
const AppModule = lazy(() => import('./admin/index'));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
@ -24,7 +24,7 @@ export default function App() {
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/auth/*" element={<AuthApp />} />
|
<Route path="/auth/*" element={<AuthApp />} />
|
||||||
<Route path="/app" element={<PrivateApp />} />
|
<Route path="/app/*" element={<AppModule />} />
|
||||||
<Route path="/404" element={<NotFoundPage />} />
|
<Route path="/404" element={<NotFoundPage />} />
|
||||||
<Route path="/403" element={<ForbiddenAccessPage />} />
|
<Route path="/403" element={<ForbiddenAccessPage />} />
|
||||||
<Route path="*" element={<Navigate to="/app" />} />
|
<Route path="*" element={<Navigate to="/app" />} />
|
||||||
|
|
|
@ -10,3 +10,4 @@ export const DOWLOAD_POS_LINUX_DEB_URL = import.meta.env.VITE_DOWLOAD_POS_LINUX_
|
||||||
export const DOWLOAD_POS_LINUX_SNAP_URL = import.meta.env.VITE_DOWLOAD_POS_LINUX_SNAP_URL;
|
export const DOWLOAD_POS_LINUX_SNAP_URL = import.meta.env.VITE_DOWLOAD_POS_LINUX_SNAP_URL;
|
||||||
|
|
||||||
export const BASE_API_URL_LOCAL = import.meta.env.VITE_BASE_API_URL_LOCAL;
|
export const BASE_API_URL_LOCAL = import.meta.env.VITE_BASE_API_URL_LOCAL;
|
||||||
|
export const ACCESS_SETTING = import.meta.env.VITE_BASE_ACCESS_SETTING;
|
||||||
|
|
Loading…
Reference in New Issue