Compare commits

..

No commits in common. "main-cloud" and "main" have entirely different histories.

13 changed files with 312 additions and 748 deletions

3
env/env.cloud vendored
View File

@ -1,6 +1,3 @@
VITE_APP_MODE=production 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_ACCESS_SETTING=Endy|dev

3
env/env.development vendored
View File

@ -1,6 +1,3 @@
VITE_APP_MODE=production VITE_APP_MODE=production
VITE_BASE_API_URL=https://api.sky.eigen.co.id/api 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_ACCESS_SETTING=Endy|dev

View File

@ -1,6 +1,3 @@
VITE_APP_MODE=production 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_ACCESS_SETTING=Endy|dev

View File

@ -1,6 +1,3 @@
VITE_APP_MODE=production VITE_APP_MODE=production
VITE_BASE_API_URL=http://103.187.147.241:30050/api VITE_BASE_API_URL=https://api.office.weplayground.id/api
VITE_BASE_API_REPORT_URL=http://103.187.147.241:30050/api VITE_BASE_API_REPORT_URL=https://api.office.weplayground.id/api
VITE_BASE_API_URL_LOCAL=http://103.187.147.241:30050/api
VITE_BASE_ACCESS_SETTING=Endy|dev

View File

@ -1,19 +1,277 @@
import { lazy } from 'react'; import axios from 'axios';
import AdminLayout from './layout'; import AdminLayout from './layout';
import { Navigate, Route, Routes } from 'react-router-dom'; import { API_URL, currencyFormatter } from '@pos/base';
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';
const ReportModule = lazy(() => import('./pages/report')); export default function Admin() {
const SettingModule = lazy(() => import('./pages/setting')); 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]);
export default function AppModule() {
return ( return (
<AdminLayout> <AdminLayout>
<Routes> <Row>
<Route path="/report" element={<ReportModule />} /> <Col xl={8} lg={8} md={12} span={24}>
<Route path="/setting" element={<SettingModule />} /> <DatePicker
<Route path="/" element={<Navigate to="/app/report" />} /> size="large"
<Route path="*" element={<Navigate to={'/404'} replace={true} />} /> popupStyle={{ fontSize: 16 }}
</Routes> 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>
</AdminLayout> </AdminLayout>
); );
} }

View File

@ -1,155 +0,0 @@
import dayjs from 'dayjs';
import { Button, Card, Col, Flex, InputNumber, Modal, Row, Form } from 'antd';
import { Fragment, useEffect, useState } from 'react';
import axios from 'axios';
import { notificationError } from '@pos/base';
export default function DefaultValue() {
const [loading, setLoading] = useState(false);
const [loadingSave, setLoadingSave] = useState(false);
const [defaultPercentage, setDefaultPercentage] = useState<number>(100);
const [currentPercentage, setCurrentPercentage] = useState<number>(100);
const [form] = Form.useForm();
const [openModal, setOpenModal] = useState(false);
const onCloseModal = async () => {
await form.resetFields();
setOpenModal(false);
};
const onOpenModal = async () => {
await form.setFieldsValue({ default_value: defaultPercentage });
setOpenModal(true);
};
const handleSaveData = async () => {
try {
const payload = await form.validateFields();
setLoadingSave(true);
await axios
.post('v1/data-scheduling-default', payload)
.then(async (resp) => {
const value = resp.data?.data?.default_value;
await form.setFieldsValue({ default_value: value });
setDefaultPercentage(value);
setLoadingSave(false);
onCloseModal();
})
.catch((err) => {
const message = err.message;
if (message) notificationError(message);
setLoadingSave(false);
});
} catch (error) {
console.error(error);
}
};
const handleGetData = async () => {
setLoading(true);
await Promise.all([
axios
.get('v1/data-scheduling-default')
.then((resp) => {
const value = resp.data?.data?.default_value;
form.setFieldsValue({ default_value: value });
setDefaultPercentage(value);
})
.catch((err) => {
const message = err.message;
if (message) notificationError(message);
}),
axios
.get('v1/data-scheduling-active', { params: { date: dayjs().format('YYYY-MM-DD') } })
.then((resp) => {
const value = resp.data?.data?.value;
form.setFieldsValue({ value: value });
setCurrentPercentage(value);
})
.catch((err) => {
const message = err.message;
if (message) notificationError(message);
}),
]);
setLoading(false);
};
useEffect(() => {
handleGetData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<Modal
open={openModal}
onCancel={onCloseModal}
onOk={handleSaveData}
okButtonProps={{ loading: loadingSave }}
cancelButtonProps={{ disabled: loadingSave }}
okText="Save"
title={<div className="text-lg font-bold">DEFAULT PERCENTAGE</div>}
>
<Form form={form}>
<Flex justify="center">
<Flex align="baseline">
<Form.Item
name={'default_value'}
rules={[
{ required: true, message: 'Value harus diisi!' },
{ type: 'number', max: 100, message: 'Value maksimal 100%!' },
{
validator(_, value) {
if (value <= 0) return Promise.reject(new Error('Value harus lebih dari 0!'));
return Promise.resolve();
},
},
]}
>
<InputNumber style={{ fontSize: 45, width: 120, fontWeight: 'bold' }} />
</Form.Item>
<div className="text-[50px] font-extrabold ml-4">{`%`}</div>
</Flex>
</Flex>
<div style={{ fontSize: 11, fontWeight: 400, color: 'rgba(0,0,0,0.4)', fontStyle: 'italic' }}>
{`*Value akan diterapkan otomatis jika tidak ada pengaturan khusus pada tanggal tersebut.`}
</div>
</Form>
</Modal>
<Row gutter={[8, 8]}>
<Col xl={4} span={12}>
<Card loading={loading} styles={{ body: { padding: '6px 12px 6px 12px' } }}>
<Flex vertical gap={8} justify="space-between">
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>DEFAULT PERCENTAGE</div>
<Flex gap={12}>
<div
style={{ fontSize: 24, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}
>{`${defaultPercentage >= 0 ? `${defaultPercentage} %` : '-'}`}</div>
<Button type="link" onClick={onOpenModal}>
Edit
</Button>
</Flex>
<div style={{ fontSize: 11, fontWeight: 400, color: 'rgba(0,0,0,0.4)', fontStyle: 'italic' }}>
{`Value otomatis jika tak ada setelan khusus.`}
</div>
</Flex>
</Card>
</Col>
<Col xl={4} span={12}>
<Card loading={loading} styles={{ body: { padding: '6px 12px 6px 12px' } }}>
<Flex vertical gap={8} justify="space-between">
<div style={{ fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.4)' }}>CURRENT PERCENTAGE</div>
<div
style={{ fontSize: 24, fontWeight: 600, color: 'rgba(0,0,0,0.6)' }}
>{`${currentPercentage >= 0 ? `${currentPercentage} %` : '-'}`}</div>
<div style={{ fontSize: 11, fontWeight: 400, color: 'rgba(0,0,0,0.4)', fontStyle: 'italic' }}>
{`Value yang di terapkan hari ini ${dayjs().format('DD-MM-YYYY')}.`}
</div>
</Flex>
</Card>
</Col>
</Row>
</Fragment>
);
}

View File

@ -1,144 +0,0 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
import { App, Button, Col, InputNumber, InputNumberProps, Modal, ModalProps, Row, Slider } from 'antd';
import { API_URL, BASE_API_URL_LOCAL } from '@pos/base';
export default function LocalDataConfiguration(modalProps: ModalProps) {
const { modal, notification } = App.useApp();
const [configData, setConfigData] = useState<any>();
const [inputValue, setInputValue] = useState(0);
const [loadingGet, setLoadingGet] = useState(false);
const [loadingSave, setLoadingSave] = useState(false);
const onChange: InputNumberProps['onChange'] = (newValue) => {
setInputValue(newValue as number);
};
function makeColorText(value: number) {
if (value <= 30) {
return 'text-red-500';
} else if (value > 30 && value <= 80) {
return 'text-yellow-500';
} else if (value > 80 && value <= 100) {
return 'text-green-500';
}
}
async function handleSave(value: number) {
setLoadingSave(true);
await axios
.put(API_URL.EDIT_TRANSACTION_SETTING, { id: configData?.id, value }, { baseURL: BASE_API_URL_LOCAL })
.then(() => {
notification.success({
message: 'Sukses',
description: 'Konfigurasi berhasil disimpan',
});
if (modalProps.onCancel) modalProps.onCancel(null as any);
})
.catch((err) => {
notification.error({
message: 'Gagal',
description: err?.message ?? err?.response?.data?.message ?? 'Terjadi kesalahan saat menyimpan konfigurasi',
});
})
.finally(() => {
setLoadingSave(false);
});
}
async function handleGetData() {
setLoadingGet(true);
await axios
.get(API_URL.GET_TRANSACTION_SETTING, { baseURL: BASE_API_URL_LOCAL })
.then((res) => {
const data = res.data.data;
const respValue = data?.value;
setInputValue(Number(respValue ?? '0'));
setConfigData(data);
})
.catch((err) => {
notification.error({
message: 'Gagal',
description: err?.message ?? err?.response?.data?.message ?? 'Terjadi kesalahan saat mengambil konfigurasi',
});
})
.finally(() => {
setLoadingGet(false);
});
}
useEffect(() => {
if (modalProps.open) handleGetData();
}, [modalProps.open]);
return (
<Modal
{...modalProps}
title="KONFIGURASI TAMPILAN DATA LOKAL"
width={800}
footer={[
<Button key="cancel" onClick={modalProps.onCancel} style={{ width: 100 }} disabled={loadingSave || loadingGet}>
Batal
</Button>,
<Button
key="submit"
type="primary"
disabled={loadingSave || loadingGet}
onClick={() => {
modal.confirm({
title: 'KONFIRMASI',
icon: null,
cancelText: 'Batal',
cancelButtonProps: { style: { width: 100 } },
okButtonProps: { style: { width: 100 } },
content: (
<div>
<div>Apakah Anda yakin ingin menyimpan konfigurasi ini?</div>
<div className="mb-4">Jumlah data yang akan ditampilkan di aplikasi lokal adalah: </div>
<div className={`text-center font-bold text-6xl mb-6 ${makeColorText(inputValue)}`}>
{inputValue}%
</div>
</div>
),
onOk: () => handleSave(inputValue),
});
}}
>
Simpan
</Button>,
]}
>
<div className="text-gray-600 italic mb-6">{`Gunakan slider atau input number dibawah ini untuk menentukan persentase data yang ingin ditampilkan di aplikasi lokal Anda. Semakin tinggi persentasenya, semakin banyak data yang akan ditampilkan. Sesuaikan dengan kebutuhan untuk memastikan performa aplikasi tetap optimal.`}</div>
<div>
<Row gutter={[32, 8]}>
<Col span={24}>
<InputNumber
disabled={loadingSave || loadingGet}
min={0}
max={100}
value={inputValue}
onChange={onChange}
addonAfter="%"
/>
</Col>
<Col span={24}>
<Slider
min={0}
max={100}
disabled={loadingSave || loadingGet}
onChange={onChange}
value={typeof inputValue === 'number' ? inputValue : 0}
marks={{
0: '0%',
25: '25%',
50: '50%',
75: '75%',
100: '100%',
}}
/>
</Col>
</Row>
</div>
</Modal>
);
}

View File

@ -1,58 +1,33 @@
import { App, Avatar, Dropdown, Flex, Image, Layout } from 'antd'; import { Avatar, Flex, Image, Layout, Popconfirm, Tooltip } 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 { ACCESS_SETTING, API_URL, getInitialName, handleLogout, UserDataState } from '@pos/base'; import { 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 { 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 { children } = props; const { children } = props;
const [_loadingLogout, setLoadingLogout] = useState(false);
const user = useRecoilValue(UserDataState); const user = useRecoilValue(UserDataState);
const initialName = getInitialName(user?.name ?? ''); const initialName = getInitialName(user?.name ?? '');
const [openModalConfig, setOpenModalConfig] = useState(false);
const onCancelModalConfig = () => setOpenModalConfig(false);
// const onOpenModalConfig = () => setOpenModalConfig(true);
async function handleClickLogout() { async function handleClickLogout() {
setLoadingLogout(true);
try { try {
await axios({ url: `${API_URL.LOGOUT}`, method: 'delete' }); await axios({ url: `${API_URL.LOGOUT}`, method: 'delete' });
setLoadingLogout(false);
await handleLogout(); await handleLogout();
} catch (err: any) { } catch (err: any) {
console.error(err); setLoadingLogout(false);
} }
} }
function checkAllowAccessSetting() {
const username = user?.username;
const accessSetting = ACCESS_SETTING ?? '';
const allowList = accessSetting.split('|');
if (allowList.includes(username)) return true;
return false;
}
function gotoHome() {
navigate('/app');
}
function gotoSetting() {
// onOpenModalConfig();
navigate('/app/setting');
}
return ( return (
<Layout> <Layout>
<Header <Header
@ -63,78 +38,35 @@ export default function AdminLayout(props: AdminLayoutProps) {
width: '100%', width: '100%',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
// background: token.colorPrimary,
background: '#fff', background: '#fff',
borderBottom: '1px solid #ddd', borderBottom: '1px solid #ddd',
}} }}
> >
<Flex style={{ width: '100%' }} align="center"> <Flex style={{ width: '100%' }} align="center">
<Flex> <Flex>
<Image src={Logo} width={50} preview={false} onClick={gotoHome} style={{ cursor: 'pointer' }} /> <Image src={Logo} width={50} preview={false} />
{/* <div style={{ fontWeight: 800, fontSize: 16 }}>WE POS</div> */}
</Flex> </Flex>
<Flex style={{ marginLeft: 'auto' }} gap={15} align="center"> <Flex style={{ marginLeft: 'auto' }}>
<Dropdown <Popconfirm
trigger={['click']} title="Are you sure want to logout?"
overlayStyle={{ width: 150 }} placement="bottomRight"
menu={{ onConfirm={() => handleClickLogout()}
items: [
{
key: '1',
label: 'Report',
icon: <FileTextOutlined />,
onClick: () => gotoHome(),
},
{
type: 'divider',
key: 'divider_1',
},
{
key: '2',
label: 'Setting',
icon: <SettingOutlined />,
onClick: () => gotoSetting(),
},
{
type: 'divider',
key: 'divider_2',
},
{
key: '3',
label: 'Logout',
icon: <LogoutOutlined />,
onClick: () => {
modal.confirm({
icon: null,
cancelText: 'Batal',
cancelButtonProps: { style: { width: 100 } },
okButtonProps: { style: { width: 100 } },
content: <div>Apakah anda yakin ingin keluar dari aplikasi?</div>,
onOk: () => handleClickLogout(),
});
},
},
]
.map((item) => {
const isAllowSetting = checkAllowAccessSetting();
if (!isAllowSetting && ['1', '2', 'divider_1', 'divider_2'].includes(item.key)) {
return undefined;
}
return item;
})
.filter(Boolean) as any,
}}
> >
<Avatar size={35} style={{ cursor: 'pointer' }}> <Tooltip title="Logout" placement="bottom">
{initialName ? ( <Avatar size={35}>
<div style={{ fontSize: 13 }}>{initialName?.toUpperCase()}</div> {initialName ? (
) : ( <div style={{ fontSize: 13 }}>{initialName?.toUpperCase()}</div>
<FaUser style={{ fontSize: 14 }} /> ) : (
)} <FaUser style={{ fontSize: 14 }} />
</Avatar> )}
</Dropdown> </Avatar>
</Tooltip>
</Popconfirm>
</Flex> </Flex>
</Flex> </Flex>
</Header> </Header>
<LocalDataConfiguration open={openModalConfig} onCancel={onCancelModalConfig} />
<Content style={{ overflow: 'initial', background: '#fff', padding: 10 }}>{children}</Content> <Content style={{ overflow: 'initial', background: '#fff', padding: 10 }}>{children}</Content>
</Layout> </Layout>
); );

View File

@ -1,276 +0,0 @@
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>
);
}

View File

@ -1,31 +0,0 @@
import { ACCESS_SETTING, UserDataState } from '@pos/base';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import DefaultValue from '../layout/components/default-value';
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>
<DefaultValue />
</div>
);
}

View File

@ -1,36 +1,34 @@
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { ConfigProvider, Flex, Spin, App as AntdApp } from 'antd'; import { ConfigProvider, Flex, Spin } from 'antd';
import { DebugObserver, ForbiddenAccessPage, NotFoundPage } from '@pos/base'; import { DebugObserver, ForbiddenAccessPage, NotFoundPage } from '@pos/base';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { APP_THEME } from '@pos/base/presentation/assets/themes'; 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 AppModule = lazy(() => import('./admin/index')); const PrivateApp = lazy(() => import('./admin'));
export default function App() { export default function App() {
return ( return (
<RecoilRoot> <RecoilRoot>
<DebugObserver /> <DebugObserver />
<ConfigProvider theme={APP_THEME.LIGHT}> <ConfigProvider theme={APP_THEME.LIGHT}>
<AntdApp> <Suspense
<Suspense fallback={
fallback={ <Flex align="center" justify="center" style={{ height: '100vh' }}>
<Flex align="center" justify="center" style={{ height: '100vh' }}> <Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} /> </Flex>
</Flex> }
} >
> <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" />} /> </Routes>
</Routes> </Suspense>
</Suspense>
</AntdApp>
</ConfigProvider> </ConfigProvider>
</RecoilRoot> </RecoilRoot>
); );

View File

@ -5,7 +5,4 @@ export const API_URL = {
REPORT_SUMMARY_INCOME_ITEM: '/v1/report-summary/income-item', REPORT_SUMMARY_INCOME_ITEM: '/v1/report-summary/income-item',
REPORT_SUMMARY_INCOME_ITEM_MASTER: '/v1/report-summary/income-item-master', REPORT_SUMMARY_INCOME_ITEM_MASTER: '/v1/report-summary/income-item-master',
EDIT_TRANSACTION_SETTING: '/v1/transaction-setting',
GET_TRANSACTION_SETTING: '/v1/transaction-setting/detail',
}; };

View File

@ -8,6 +8,3 @@ export const EMBED_DASHBOARD_ID = import.meta.env.VITE_EMBED_DASHBOARD_ID;
export const DOWLOAD_POS_WINDOWS_URL = import.meta.env.VITE_DOWLOAD_POS_WINDOWS_URL; export const DOWLOAD_POS_WINDOWS_URL = import.meta.env.VITE_DOWLOAD_POS_WINDOWS_URL;
export const DOWLOAD_POS_LINUX_DEB_URL = import.meta.env.VITE_DOWLOAD_POS_LINUX_DEB_URL; export const DOWLOAD_POS_LINUX_DEB_URL = import.meta.env.VITE_DOWLOAD_POS_LINUX_DEB_URL;
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 ACCESS_SETTING = import.meta.env.VITE_BASE_ACCESS_SETTING;