commit
4e4214655c
|
@ -1,3 +1,6 @@
|
|||
VITE_APP_MODE=production
|
||||
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_URL_LOCAL=https://api.office.weplayground.id/api
|
||||
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -1,3 +1,6 @@
|
|||
VITE_APP_MODE=production
|
||||
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_URL_LOCAL=https://api.sky.eigen.co.id/api
|
||||
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -1,3 +1,6 @@
|
|||
VITE_APP_MODE=production
|
||||
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_URL_LOCAL=http://172.16.2.101:30050/api
|
||||
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -1,3 +1,6 @@
|
|||
VITE_APP_MODE=production
|
||||
VITE_BASE_API_URL=https://api.office.weplayground.id/api
|
||||
VITE_BASE_API_REPORT_URL=https://api.office.weplayground.id/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_URL_LOCAL=http://103.187.147.241:30050/api
|
||||
VITE_BASE_ACCESS_SETTING=Endy|dev
|
|
@ -10,10 +10,10 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^5.21.2",
|
||||
"ag-grid-community": "^31.3.2",
|
||||
"ag-grid-enterprise": "^31.3.2",
|
||||
"ag-grid-react": "^31.3.2",
|
||||
"antd": "^5.21.2",
|
||||
"axios": "^1.7.7",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
|
|
|
@ -1,277 +1,19 @@
|
|||
import axios from 'axios';
|
||||
import { lazy } from 'react';
|
||||
import AdminLayout from './layout';
|
||||
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';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
export default function Admin() {
|
||||
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]);
|
||||
const ReportModule = lazy(() => import('./pages/report'));
|
||||
const SettingModule = lazy(() => import('./pages/setting'));
|
||||
|
||||
export default function AppModule() {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<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>
|
||||
<Routes>
|
||||
<Route path="/report" element={<ReportModule />} />
|
||||
<Route path="/setting" element={<SettingModule />} />
|
||||
<Route path="/" element={<Navigate to="/app/report" />} />
|
||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||
</Routes>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,33 +1,58 @@
|
|||
import { Avatar, Flex, Image, Layout, Popconfirm, Tooltip } from 'antd';
|
||||
import { App, Avatar, Dropdown, Flex, Image, Layout } from 'antd';
|
||||
import { ReactNode, useState } from 'react';
|
||||
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 axios from 'axios';
|
||||
import Logo from '../../../base/presentation/assets/images/we-logo.png';
|
||||
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 {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AdminLayout(props: AdminLayoutProps) {
|
||||
const navigate = useNavigate();
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const { children } = props;
|
||||
const [_loadingLogout, setLoadingLogout] = useState(false);
|
||||
const user = useRecoilValue(UserDataState);
|
||||
const initialName = getInitialName(user?.name ?? '');
|
||||
|
||||
const [openModalConfig, setOpenModalConfig] = useState(false);
|
||||
const onCancelModalConfig = () => setOpenModalConfig(false);
|
||||
// const onOpenModalConfig = () => setOpenModalConfig(true);
|
||||
|
||||
async function handleClickLogout() {
|
||||
setLoadingLogout(true);
|
||||
try {
|
||||
await axios({ url: `${API_URL.LOGOUT}`, method: 'delete' });
|
||||
setLoadingLogout(false);
|
||||
await handleLogout();
|
||||
} catch (err: any) {
|
||||
setLoadingLogout(false);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Layout>
|
||||
<Header
|
||||
|
@ -38,35 +63,78 @@ export default function AdminLayout(props: AdminLayoutProps) {
|
|||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
// background: token.colorPrimary,
|
||||
background: '#fff',
|
||||
borderBottom: '1px solid #ddd',
|
||||
}}
|
||||
>
|
||||
<Flex style={{ width: '100%' }} align="center">
|
||||
<Flex>
|
||||
<Image src={Logo} width={50} preview={false} />
|
||||
{/* <div style={{ fontWeight: 800, fontSize: 16 }}>WE POS</div> */}
|
||||
<Image src={Logo} width={50} preview={false} onClick={gotoHome} style={{ cursor: 'pointer' }} />
|
||||
</Flex>
|
||||
<Flex style={{ marginLeft: 'auto' }}>
|
||||
<Popconfirm
|
||||
title="Are you sure want to logout?"
|
||||
placement="bottomRight"
|
||||
onConfirm={() => handleClickLogout()}
|
||||
<Flex style={{ marginLeft: 'auto' }} gap={15} align="center">
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlayStyle={{ width: 150 }}
|
||||
menu={{
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Logout" placement="bottom">
|
||||
<Avatar size={35}>
|
||||
{initialName ? (
|
||||
<div style={{ fontSize: 13 }}>{initialName?.toUpperCase()}</div>
|
||||
) : (
|
||||
<FaUser style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
<Avatar size={35} style={{ cursor: 'pointer' }}>
|
||||
{initialName ? (
|
||||
<div style={{ fontSize: 13 }}>{initialName?.toUpperCase()}</div>
|
||||
) : (
|
||||
<FaUser style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</Avatar>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Header>
|
||||
<LocalDataConfiguration open={openModalConfig} onCancel={onCancelModalConfig} />
|
||||
<Content style={{ overflow: 'initial', background: '#fff', padding: 10 }}>{children}</Content>
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
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';
|
||||
import { makeColorTextValue } from '../helpers';
|
||||
|
||||
export default function DefaultValue() {
|
||||
const [loadingDefault, setLoadingDefault] = useState(false);
|
||||
const [loadingActive, setLoadingActive] = 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 });
|
||||
await handleGetDataActive();
|
||||
setDefaultPercentage(value);
|
||||
setLoadingSave(false);
|
||||
onCloseModal();
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err.message;
|
||||
if (message) notificationError(message);
|
||||
setLoadingSave(false);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetDataActive = async () => {
|
||||
setLoadingActive(true);
|
||||
await 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);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingActive(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetDataDefault = async () => {
|
||||
setLoadingDefault(true);
|
||||
await 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);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingDefault(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetData = async () => {
|
||||
await Promise.all([handleGetDataDefault(), handleGetDataActive()]);
|
||||
};
|
||||
|
||||
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 span={12}>
|
||||
<Card loading={loadingDefault} 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 }}
|
||||
className={`${makeColorTextValue(defaultPercentage)}`}
|
||||
>{`${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 span={12}>
|
||||
<Card loading={loadingActive} 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>
|
||||
<Flex gap={12}>
|
||||
<div
|
||||
style={{ fontSize: 24, fontWeight: 600 }}
|
||||
className={`${makeColorTextValue(currentPercentage)}`}
|
||||
>{`${currentPercentage >= 0 ? `${currentPercentage} %` : '-'}`}</div>
|
||||
<Button type="link" onClick={handleGetDataActive}>
|
||||
Reload
|
||||
</Button>
|
||||
</Flex>
|
||||
<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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,430 @@
|
|||
import dayjs from 'dayjs';
|
||||
import axios from 'axios';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import {
|
||||
App,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Flex,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
List,
|
||||
Modal,
|
||||
notification,
|
||||
Pagination,
|
||||
Row,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { capitalizeEachWord, makeColorStatus, STATUS_DATA } from '@pos/base';
|
||||
|
||||
import { DeleteOutlined, EditOutlined, LockOutlined, PlusOutlined, UnlockOutlined } from '@ant-design/icons';
|
||||
import { makeColorTextValue } from '../helpers';
|
||||
|
||||
export default function SchedulingData() {
|
||||
const [formModal] = Form.useForm();
|
||||
const { modal } = App.useApp();
|
||||
const [openModalForm, setOpenModalForm] = useState(false);
|
||||
|
||||
const [loadingTable, setLoadingTable] = useState(false);
|
||||
const [schedulingData, setSchedulingData] = useState<any[]>([]);
|
||||
const [schedulingMeta, setSchedulingMeta] = useState<any>();
|
||||
|
||||
const [loadingForm, setLoadingForm] = useState(false);
|
||||
|
||||
const handleGetData = async (page: number) => {
|
||||
setLoadingTable(true);
|
||||
await axios
|
||||
.get('v1/data-scheduling', {
|
||||
params: {
|
||||
page: page,
|
||||
limit: 10,
|
||||
order_type: 'ASC',
|
||||
order_by: 'schedule_date_from',
|
||||
schedule_date_from: dayjs().format('YYYY-MM-DD'),
|
||||
},
|
||||
})
|
||||
.then((resp: any) => {
|
||||
const data = resp?.data?.data ?? [];
|
||||
const meta = resp?.data?.meta;
|
||||
setSchedulingMeta(meta);
|
||||
setSchedulingData(data);
|
||||
setLoadingTable(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoadingTable(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleActivate = async (id: string) => {
|
||||
await axios
|
||||
.patch(`v1/data-scheduling/${id}/active`)
|
||||
.then(() => {
|
||||
handleGetData(schedulingMeta?.currentPage);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
notification.error({
|
||||
message: 'Gagal mengaktifkan data.',
|
||||
description:
|
||||
err?.message ?? err?.response?.data?.message ?? 'Terjadi kesalahan saat mengaktifkan konfigurasi',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeactivate = async (id: string) => {
|
||||
await axios
|
||||
.patch(`v1/data-scheduling/${id}/inactive`)
|
||||
.then(() => {
|
||||
handleGetData(schedulingMeta?.currentPage);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
notification.error({
|
||||
message: 'Gagal menonaktifkan data.',
|
||||
description:
|
||||
err?.message ?? err?.response?.data?.message ?? 'Terjadi kesalahan saat menonaktifkan konfigurasi',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await axios
|
||||
.delete(`v1/data-scheduling/${id}`)
|
||||
.then(() => {
|
||||
handleGetData(schedulingMeta?.currentPage);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
notification.error({
|
||||
message: 'Gagal menghapus data.',
|
||||
description: err?.message ?? err?.response?.data?.message ?? 'Terjadi kesalahan saat menghapus konfigurasi',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickUpdate = async (item: any) => {
|
||||
await formModal.setFieldsValue({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
indexing_key: item.indexing_key,
|
||||
schedule_date_from: dayjs(item.schedule_date_from),
|
||||
schedule_date_to: dayjs(item.schedule_date_from),
|
||||
});
|
||||
setOpenModalForm(true);
|
||||
};
|
||||
|
||||
const handleClickCreate = async () => {
|
||||
await formModal.resetFields();
|
||||
setOpenModalForm(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = async () => {
|
||||
await formModal.resetFields();
|
||||
setOpenModalForm(false);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async () => {
|
||||
const formValues = await formModal.validateFields();
|
||||
const dataID = formValues.id;
|
||||
|
||||
const payload = {
|
||||
name: formValues.name,
|
||||
indexing_key: formValues.indexing_key,
|
||||
schedule_date_from: formValues.schedule_date_from && dayjs(formValues.schedule_date_from).format('YYYY-MM-DD'),
|
||||
schedule_date_to: formValues.schedule_date_from && dayjs(formValues.schedule_date_from).format('YYYY-MM-DD'),
|
||||
};
|
||||
|
||||
if (dataID) handleEdit(dataID, payload);
|
||||
else handleCreate(payload);
|
||||
};
|
||||
|
||||
const handleCreate = async (payload: any) => {
|
||||
setLoadingForm(true);
|
||||
await axios
|
||||
.post(`v1/data-scheduling`, payload)
|
||||
.then(async () => {
|
||||
await handleGetData(1);
|
||||
await handleCloseModal();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
notification.error({
|
||||
message: 'Gagal menyimpan data.',
|
||||
description:
|
||||
err?.message ?? err?.response?.data?.message ?? 'Terjadi kesalahan saat mengaktifkan konfigurasi',
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingForm(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = async (id: string, payload: any) => {
|
||||
setLoadingForm(true);
|
||||
await axios
|
||||
.put(`v1/data-scheduling/${id}`, payload)
|
||||
.then(async () => {
|
||||
await handleGetData(schedulingMeta?.currentPage);
|
||||
await handleCloseModal();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
notification.error({
|
||||
message: 'Gagal menyimpan data.',
|
||||
description:
|
||||
err?.message ?? err?.response?.data?.message ?? 'Terjadi kesalahan saat mengaktifkan konfigurasi',
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingForm(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleGetData(1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
open={openModalForm}
|
||||
onCancel={handleCloseModal}
|
||||
cancelButtonProps={{ disabled: loadingForm }}
|
||||
okText="Simpan"
|
||||
okButtonProps={{ loading: loadingForm }}
|
||||
onOk={handleSubmitModal}
|
||||
title="FORM KONFIGURASI PENJADWALAN"
|
||||
>
|
||||
<Form form={formModal} layout="vertical">
|
||||
<Form.Item name={['id']} noStyle></Form.Item>
|
||||
<Row gutter={[12, 1]}>
|
||||
<Col xs={24} sm={12} span={12}>
|
||||
<Form.Item name={['name']} label="Label" rules={[{ required: true, message: 'Label harus diisi!' }]}>
|
||||
<Input placeholder="Input label" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} span={12}>
|
||||
<Form.Item
|
||||
name={['indexing_key']}
|
||||
label="Total Data"
|
||||
rules={[
|
||||
{ required: true, message: 'Total data harus diisi!' },
|
||||
{ type: 'number', max: 100, message: 'Total data maksimal 100%!' },
|
||||
{
|
||||
validator(_, value) {
|
||||
if (value <= 0) return Promise.reject(new Error('Total data harus lebih dari 0!'));
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} addonAfter="%" placeholder="Input value" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} span={12}>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{({ getFieldsValue }) => {
|
||||
const values = getFieldsValue();
|
||||
const endDate = values?.schedule_date_to;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label="Start Date"
|
||||
name={'schedule_date_from'}
|
||||
rules={[{ required: true, message: 'Start date harus diisi!' }]}
|
||||
>
|
||||
<DatePicker
|
||||
showNow={false}
|
||||
style={{ width: '100%' }}
|
||||
size="large"
|
||||
disabledDate={(date) => {
|
||||
if (endDate)
|
||||
return (
|
||||
(!date.isBefore(endDate) && !date.isSame(endDate)) ||
|
||||
date.isBefore(dayjs().subtract(1, 'day'))
|
||||
);
|
||||
else return date.isBefore(dayjs().subtract(1, 'day'));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} span={12}>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{({ getFieldsValue }) => {
|
||||
const values = getFieldsValue();
|
||||
const startDate = values?.schedule_date_from;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label="End Date"
|
||||
name={'schedule_date_to'}
|
||||
rules={[{ required: true, message: 'End date harus diisi!' }]}
|
||||
>
|
||||
<DatePicker
|
||||
showNow={false}
|
||||
style={{ width: '100%' }}
|
||||
size="large"
|
||||
disabledDate={(date) => {
|
||||
if (startDate) return !date.isAfter(startDate) && !date.isSame(startDate);
|
||||
else return date.isBefore(dayjs().subtract(1, 'day'));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
<div>
|
||||
<Flex justify="end">
|
||||
<Button type="primary" onClick={handleClickCreate} icon={<PlusOutlined />}>
|
||||
Tambah Jadwal
|
||||
</Button>
|
||||
</Flex>
|
||||
{schedulingData?.length > 0 && (
|
||||
<Fragment>
|
||||
<Divider style={{ margin: 10 }} />
|
||||
</Fragment>
|
||||
)}
|
||||
<div style={{ marginBottom: 10 }}></div>
|
||||
<List
|
||||
bordered={!schedulingData || schedulingData?.length <= 0}
|
||||
style={{ maxHeight: 500, overflow: 'auto' }}
|
||||
dataSource={schedulingData}
|
||||
loading={loadingTable}
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.id} style={{ borderBlockEnd: 'none', padding: 5 }}>
|
||||
<Card className="w-full">
|
||||
<Flex className="w-full" justify="space-between">
|
||||
<Row className="w-full" gutter={[4, 8]}>
|
||||
<Col xs={18} sm={12} span={12}>
|
||||
<Flex align="center" className="h-full" gap={4}>
|
||||
<div>
|
||||
<div className="text-xs mb-2" style={{ color: makeColorStatus(item.status) }}>
|
||||
{capitalizeEachWord(item.status)}
|
||||
</div>
|
||||
<div className="text-[#00000099]">{item.name}</div>
|
||||
<div className="text-[#00000099]">{`${dayjs(item?.schedule_date_from).format('DD MMM YYYY')} - ${dayjs(item?.schedule_date_to).format('DD MMM YYYY')}`}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</Col>
|
||||
<Col xs={18} sm={12} span={12}>
|
||||
<Flex align="center" className="h-full" gap={8}>
|
||||
<div
|
||||
className={`text-xl font-bold ${makeColorTextValue(item.indexing_key)}`}
|
||||
>{`${item.indexing_key} %`}</div>
|
||||
</Flex>
|
||||
</Col>
|
||||
</Row>
|
||||
<Flex align="center" gap={4}>
|
||||
<Tooltip title="Edit" trigger={'hover'} placement="bottom">
|
||||
<Button icon={<EditOutlined />} onClick={() => handleClickUpdate(item)} />
|
||||
</Tooltip>
|
||||
|
||||
{item.status === STATUS_DATA.ACTIVE && (
|
||||
<Tooltip title="Inactive" trigger={'hover'} placement="bottom">
|
||||
<Button
|
||||
icon={<LockOutlined />}
|
||||
onClick={() => {
|
||||
modal.confirm({
|
||||
icon: null,
|
||||
cancelText: 'Batal',
|
||||
title: 'Nonaktifkan Konfigurasi Penjadwalan?',
|
||||
cancelButtonProps: { style: { width: 100 } },
|
||||
okButtonProps: { style: { width: 100 } },
|
||||
content: (
|
||||
<div>
|
||||
<div className="font-semibold italic">{`Perhatian: `}</div>
|
||||
<div className="italic">{`Konfigurasi yang dinonaktifkan tidak akan diikutsertakan dalam proses penjadwalan hingga Anda mengaktifkannya kembali. Ini berguna untuk menjeda proses penjadwalan tertentu tanpa menghapus konfigurasi.`}</div>
|
||||
</div>
|
||||
),
|
||||
onOk: () => handleDeactivate(item.id),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{item.status === STATUS_DATA.INACTIVE && (
|
||||
<Tooltip title="Active" trigger={'hover'} placement="bottom">
|
||||
<Button
|
||||
icon={<UnlockOutlined />}
|
||||
onClick={() => {
|
||||
modal.confirm({
|
||||
icon: null,
|
||||
cancelText: 'Batal',
|
||||
title: 'Aktifkan Konfigurasi Penjadwalan?',
|
||||
cancelButtonProps: { style: { width: 100 } },
|
||||
okButtonProps: { style: { width: 100 } },
|
||||
content: (
|
||||
<div>
|
||||
<div className="font-semibold italic">{`Perhatian: `}</div>
|
||||
<div className="italic">{`Dengan mengaktifkannya, konfigurasi ini akan kembali diperhitungkan dan digunakan dalam proses penjadwalan selanjutnya, sesuai dengan kriteria yang telah ditetapkan.`}</div>
|
||||
</div>
|
||||
),
|
||||
onOk: () => handleActivate(item.id),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip title="Delete" trigger={'hover'} placement="bottom">
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
modal.confirm({
|
||||
icon: null,
|
||||
cancelText: 'Batal',
|
||||
title: 'Hapus Konfigurasi Penjadwalan?',
|
||||
cancelButtonProps: { style: { width: 100 } },
|
||||
okButtonProps: { style: { width: 100 } },
|
||||
content: (
|
||||
<div>
|
||||
<div className="font-semibold italic">{`Perhatian: `}</div>
|
||||
<div className="italic">{`Tindakan ini bersifat permanen. Konfigurasi yang terhapus tidak akan lagi disertakan dalam perhitungan atau proses penjadwalan di masa mendatang dan tidak dapat dipulihkan.`}</div>
|
||||
</div>
|
||||
),
|
||||
onOk: () => handleDelete(item.id),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<div style={{ marginBottom: 10 }}></div>
|
||||
{schedulingData?.length > 0 && (
|
||||
<Fragment>
|
||||
<Divider style={{}} />
|
||||
<Flex justify="end" className="mt-2">
|
||||
<Pagination
|
||||
current={schedulingMeta?.currentPage}
|
||||
total={schedulingMeta?.totalItems}
|
||||
pageSize={10}
|
||||
onChange={async (page) => {
|
||||
await handleGetData(page);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Fragment>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<div className="italic font-semibold">Informasi Penting: </div>
|
||||
<div className="italic text-[#00000066]">
|
||||
{`Daftar konfigurasi penjadwalan diurutkan otomatis berdasarkan Waktu Mulai. Konfigurasi baru akan muncul sesuai urutan tanggalnya, dan jadwal yang sudah terlewat tidak akan ditampilkan.`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export function makeColorTextValue(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';
|
||||
} else {
|
||||
return 'text-[#00000066]';
|
||||
}
|
||||
}
|
|
@ -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,36 @@
|
|||
import { ACCESS_SETTING, UserDataState } from '@pos/base';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import DefaultValue from './components/default-value';
|
||||
import SchedulingData from './components/scheduling-data';
|
||||
import { Flex } from 'antd';
|
||||
|
||||
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');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={24}>
|
||||
<DefaultValue />
|
||||
<SchedulingData />
|
||||
</Flex>
|
||||
);
|
||||
}
|
|
@ -1,34 +1,36 @@
|
|||
import { Suspense, lazy } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { ConfigProvider, Flex, Spin } from 'antd';
|
||||
import { ConfigProvider, Flex, Spin, App as AntdApp } from 'antd';
|
||||
import { DebugObserver, ForbiddenAccessPage, NotFoundPage } from '@pos/base';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { APP_THEME } from '@pos/base/presentation/assets/themes';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
const AuthApp = lazy(() => import('./auth'));
|
||||
const PrivateApp = lazy(() => import('./admin'));
|
||||
const AppModule = lazy(() => import('./admin/index'));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<DebugObserver />
|
||||
<ConfigProvider theme={APP_THEME.LIGHT}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Flex align="center" justify="center" style={{ height: '100vh' }}>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/auth/*" element={<AuthApp />} />
|
||||
<Route path="/app" element={<PrivateApp />} />
|
||||
<Route path="/404" element={<NotFoundPage />} />
|
||||
<Route path="/403" element={<ForbiddenAccessPage />} />
|
||||
<Route path="*" element={<Navigate to="/app" />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
<AntdApp>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Flex align="center" justify="center" style={{ height: '100vh' }}>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/auth/*" element={<AuthApp />} />
|
||||
<Route path="/app/*" element={<AppModule />} />
|
||||
<Route path="/404" element={<NotFoundPage />} />
|
||||
<Route path="/403" element={<ForbiddenAccessPage />} />
|
||||
<Route path="*" element={<Navigate to="/app" />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
|
|
@ -5,4 +5,7 @@ export const API_URL = {
|
|||
|
||||
REPORT_SUMMARY_INCOME_ITEM: '/v1/report-summary/income-item',
|
||||
REPORT_SUMMARY_INCOME_ITEM_MASTER: '/v1/report-summary/income-item-master',
|
||||
|
||||
EDIT_TRANSACTION_SETTING: '/v1/transaction-setting',
|
||||
GET_TRANSACTION_SETTING: '/v1/transaction-setting/detail',
|
||||
};
|
||||
|
|
|
@ -8,3 +8,6 @@ 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_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 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