diff --git a/env/env.cloud b/env/env.cloud index 0f597d3..828fa71 100644 --- a/env/env.cloud +++ b/env/env.cloud @@ -2,4 +2,5 @@ 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 \ No newline at end of file +VITE_BASE_API_URL_LOCAL=https://api.office.weplayground.id/api +VITE_BASE_ACCESS_SETTING=Endy|dev \ No newline at end of file diff --git a/env/env.development b/env/env.development index 8f8e928..1df931e 100644 --- a/env/env.development +++ b/env/env.development @@ -3,3 +3,4 @@ VITE_BASE_API_URL=https://api.sky.eigen.co.id/api VITE_BASE_API_REPORT_URL=https://api.sky.eigen.co.id/api VITE_BASE_API_URL_LOCAL=https://api.sky.eigen.co.id/api +VITE_BASE_ACCESS_SETTING=Endy|dev \ No newline at end of file diff --git a/env/env.production-offline b/env/env.production-offline index 1050e15..f6a4172 100644 --- a/env/env.production-offline +++ b/env/env.production-offline @@ -2,4 +2,5 @@ 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 \ No newline at end of file +VITE_BASE_API_URL_LOCAL=http://172.16.2.101:30050/api +VITE_BASE_ACCESS_SETTING=Endy|dev \ No newline at end of file diff --git a/env/env.production-online b/env/env.production-online index 664a215..8d1c0b9 100644 --- a/env/env.production-online +++ b/env/env.production-online @@ -2,4 +2,5 @@ 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=http://103.187.147.241:30050/api \ No newline at end of file +VITE_BASE_API_URL_LOCAL=http://103.187.147.241:30050/api +VITE_BASE_ACCESS_SETTING=Endy|dev \ No newline at end of file diff --git a/src/apps/admin/index.tsx b/src/apps/admin/index.tsx index 56d2981..c67900b 100644 --- a/src/apps/admin/index.tsx +++ b/src/apps/admin/index.tsx @@ -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([]); - const [dataItemKeys, setDataItemKeys] = useState([]); - const [loadingDataItem, setLoadingDataItem] = useState(false); - const [dataItemTotalPax, setDataItemTotalPax] = useState(0); - const [dataItemTotalRevenue, setDataItemTotalRevenue] = useState(0); - - const [dataItemMaster, setDataItemMaster] = useState([]); - const [dataItemMasterKeys, setDataItemMasterKeys] = useState([]); - const [loadingDataItemMaster, setLoadingDataItemMaster] = useState(false); - const [dataItemMasterTotalPax, setDataItemMasterTotalPax] = useState(0); - const [dataItemMasterTotalRevenue, setDataItemMasterTotalRevenue] = useState(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 ( - - - - - -
- - - - - -
{`Pendapatan Per Item ${filterDate.format('DD-MM-YYYY')}`}
- - -
{`Total revenue mungkin berbeda dengan pendapatan per item master disebabkan pengambilan harga kepada harga bundling.`}
- -
- } - > - - - -
-
TOTAL PAX
-
{dataItemTotalPax}
-
-
- - - -
-
TOTAL REVENUE
-
- {currencyFormatter({ value: dataItemTotalRevenue })} -
-
-
- -
-
- 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 }), - }, - ]} - /> - - - - - -
{`Pendapatan Per Item Master ${filterDate.format('DD-MM-YYYY')}`}
- - -
{`Total revenue mungkin berbeda dengan pendapatan per item disebabkan harga item master mengambil harga jual standard item.`}
- - - } - > - - - -
-
TOTAL PAX
-
- {dataItemMasterTotalPax} -
-
-
- - - -
-
TOTAL REVENUE
-
- {currencyFormatter({ value: dataItemMasterTotalRevenue })} -
-
-
- - -
-
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 }), - }, - ]} - /> - - - + + } /> + } /> + } /> + } /> + ); } diff --git a/src/apps/admin/layout/index.tsx b/src/apps/admin/layout/index.tsx index 81b81cb..34af951 100644 --- a/src/apps/admin/layout/index.tsx +++ b/src/apps/admin/layout/index.tsx @@ -1,19 +1,21 @@ 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 { LogoutOutlined, SettingOutlined } from '@ant-design/icons'; +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; @@ -22,7 +24,7 @@ export default function AdminLayout(props: AdminLayoutProps) { const [openModalConfig, setOpenModalConfig] = useState(false); const onCancelModalConfig = () => setOpenModalConfig(false); - const onOpenModalConfig = () => setOpenModalConfig(true); + // const onOpenModalConfig = () => setOpenModalConfig(true); async function handleClickLogout() { try { @@ -35,11 +37,22 @@ export default function AdminLayout(props: AdminLayoutProps) { function checkAllowAccessSetting() { const username = user?.username; - const allowList = ['Endy', 'superadmin']; + 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 (
- + , - onClick: () => onOpenModalConfig(), + label: 'Report', + icon: , + onClick: () => gotoHome(), }, { type: 'divider', + key: 'divider_1', }, { key: '2', + label: 'Settings', + icon: , + onClick: () => gotoSetting(), + }, + { + type: 'divider', + key: 'divider_2', + }, + { + key: '3', label: 'Logout', icon: , onClick: () => { @@ -91,7 +115,7 @@ export default function AdminLayout(props: AdminLayoutProps) { ] .map((item) => { const isAllowSetting = checkAllowAccessSetting(); - if (!isAllowSetting && (item.key === '1' || item.type === 'divider')) { + if (!isAllowSetting && ['1', '2', 'divider_1', 'divider_2'].includes(item.key)) { return undefined; } return item; diff --git a/src/apps/admin/pages/report.tsx b/src/apps/admin/pages/report.tsx new file mode 100644 index 0000000..b9832d6 --- /dev/null +++ b/src/apps/admin/pages/report.tsx @@ -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([]); + const [dataItemKeys, setDataItemKeys] = useState([]); + const [loadingDataItem, setLoadingDataItem] = useState(false); + const [dataItemTotalPax, setDataItemTotalPax] = useState(0); + const [dataItemTotalRevenue, setDataItemTotalRevenue] = useState(0); + + const [dataItemMaster, setDataItemMaster] = useState([]); + const [dataItemMasterKeys, setDataItemMasterKeys] = useState([]); + const [loadingDataItemMaster, setLoadingDataItemMaster] = useState(false); + const [dataItemMasterTotalPax, setDataItemMasterTotalPax] = useState(0); + const [dataItemMasterTotalRevenue, setDataItemMasterTotalRevenue] = useState(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 ( + + +
+ + + +
+ + +
+ + +
{`Pendapatan Per Item ${filterDate.format('DD-MM-YYYY')}`}
+ + +
{`Total revenue mungkin berbeda dengan pendapatan per item master disebabkan pengambilan harga kepada harga bundling.`}
+ + + } + > + + + +
+
TOTAL PAX
+
{dataItemTotalPax}
+
+
+ + + +
+
TOTAL REVENUE
+
+ {currencyFormatter({ value: dataItemTotalRevenue })} +
+
+
+ + +
+
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 }), + }, + ]} + /> + + + + + +
{`Pendapatan Per Item Master ${filterDate.format('DD-MM-YYYY')}`}
+ + +
{`Total revenue mungkin berbeda dengan pendapatan per item disebabkan harga item master mengambil harga jual standard item.`}
+ + + } + > + + + +
+
TOTAL PAX
+
+ {dataItemMasterTotalPax} +
+
+
+ + + +
+
TOTAL REVENUE
+
+ {currencyFormatter({ value: dataItemMasterTotalRevenue })} +
+
+
+ + +
+
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 }), + }, + ]} + /> + + + + + ); +} diff --git a/src/apps/admin/pages/setting.tsx b/src/apps/admin/pages/setting.tsx new file mode 100644 index 0000000..bb5ebc0 --- /dev/null +++ b/src/apps/admin/pages/setting.tsx @@ -0,0 +1,26 @@ +import { ACCESS_SETTING, UserDataState } from '@pos/base'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; + +export default function SettingModule() { + const user = useRecoilValue(UserDataState); + const navigate = useNavigate(); + + function checkAllowAccessSetting() { + const username = user?.username; + const accessSetting = ACCESS_SETTING ?? ''; + + const allowList = accessSetting.split('|'); + if (allowList.includes(username)) return true; + return false; + } + + useEffect(() => { + if (!checkAllowAccessSetting()) { + navigate('/app'); + } + }, []); + + return
Setting
; +} diff --git a/src/apps/index.tsx b/src/apps/index.tsx index d095cbe..627aaa0 100644 --- a/src/apps/index.tsx +++ b/src/apps/index.tsx @@ -7,7 +7,7 @@ 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 ( @@ -24,7 +24,7 @@ export default function App() { > } /> - } /> + } /> } /> } /> } /> diff --git a/src/base/infrastructure/constants/environment/index.ts b/src/base/infrastructure/constants/environment/index.ts index 64f6e2d..50d770c 100644 --- a/src/base/infrastructure/constants/environment/index.ts +++ b/src/base/infrastructure/constants/environment/index.ts @@ -10,3 +10,4 @@ export const DOWLOAD_POS_LINUX_DEB_URL = import.meta.env.VITE_DOWLOAD_POS_LINUX_ export const DOWLOAD_POS_LINUX_SNAP_URL = import.meta.env.VITE_DOWLOAD_POS_LINUX_SNAP_URL; export const BASE_API_URL_LOCAL = import.meta.env.VITE_BASE_API_URL_LOCAL; +export const ACCESS_SETTING = import.meta.env.VITE_BASE_ACCESS_SETTING;