Compare commits

...

38 Commits

Author SHA1 Message Date
Firman Ramdhani 6aec547362 feat: create page log scheduling data 2025-07-11 17:34:27 +07:00
Firman Ramdhani 7717bebbe8 feat: create page log scheduling data 2025-07-11 13:56:24 +07:00
Firman Ramdhani 4bd2b2d825 feat: create page log scheduling data 2025-07-11 11:47:05 +07:00
Firman Ramdhani 4190243416 feat: fix crud schedule 2025-07-10 18:04:26 +07:00
Firman Ramdhani cfb27f6dbc feat: remove observer 2025-07-10 14:30:54 +07:00
Firman Ramdhani 97df79cb8e feat: remove observer 2025-07-10 11:42:08 +07:00
Firman Ramdhani d330907e01 feat: display error message 2025-07-09 10:50:52 +07:00
Supan Adit Pratama 638126e06f Add env/env.development-cloud 2025-07-08 12:06:41 +07:00
Firman Ramdhani c3985de71c feat: change api active 2025-07-07 16:00:55 +07:00
firmanr 942a769873 Merge pull request 'fix: fix linter' (#3) from main-cloud into main
Reviewed-on: #3
2025-07-07 15:16:44 +07:00
Firman Ramdhani 4e6c53c0d4 fix: fix linter 2025-07-07 15:16:23 +07:00
firmanr 5b114643ae Merge pull request 'fix: chaneg env cloud' (#2) from main-cloud into main
Reviewed-on: #2
2025-07-07 15:10:42 +07:00
Firman Ramdhani ecb41d6be1 fix: chaneg env cloud 2025-07-07 15:10:06 +07:00
firmanr 4e4214655c Merge pull request 'main-cloud' (#1) from main-cloud into main
Reviewed-on: #1
2025-07-07 15:09:12 +07:00
Firman Ramdhani ec4f948082 feat: integration CRUD 2025-07-07 15:05:37 +07:00
Firman Ramdhani e150e9e416 feat: integration CRUD 2025-07-07 14:57:45 +07:00
Firman Ramdhani 9cd4d7f1a0 feat: setup crud scheduling data 2025-07-07 11:46:27 +07:00
Firman Ramdhani 1b9c656007 feat: setup crud scheduling data 2025-07-07 11:33:01 +07:00
Firman Ramdhani e737a89a24 feat(SPG-1173): setup setting scheduling on realtime report 2025-07-04 14:12:13 +07:00
Firman Ramdhani 4a08abe0b5 feat: setup setting page 2025-07-03 17:48:54 +07:00
Firman Ramdhani 29ff09d3aa feat: add setting access to change setting data 2025-05-16 10:07:20 +07:00
irfan e6481aa606 Update env/env.cloud 2025-05-13 04:52:55 +00:00
irfan 7330e38296 Update env/env.production-online 2025-05-13 04:50:20 +00:00
Firman Ramdhani 01913a739f faet: setup base url local 2025-05-02 10:22:32 +07:00
Firman Ramdhani 685d14a7df feat: create feature local data configuration 2025-05-02 10:14:53 +07:00
Firman Ramdhani d49c487e61 feat: setup config local data 2025-04-30 11:57:30 +07:00
Firman Ramdhani 537d574af7 feat: setup config local data 2025-04-30 11:55:46 +07:00
irfan 16e2c8fc09 Update env/env.cloud 2025-04-29 10:38:50 +00:00
irfan 65b3dd7cae Update env/env.cloud 2025-04-29 10:04:17 +00:00
shancheas 8f5fff0b91 change env name 2025-04-29 13:15:32 +07:00
Supan Adit Pratama e9152f11f4 chore: add production cloud 2025-04-28 12:34:02 +07:00
Supan Adit Pratama b37dff48e3 Merge branch 'main' of ssh://git.eigen.co.id:2222/eigen/pos-realtime-report
continuous-integration/drone/tag Build is passing Details
2024-10-04 06:03:29 +00:00
Supan Adit Pratama 9628070fcd ci: automation 2024-10-04 06:03:06 +00:00
Firman Ramdhani 51130437d7 Merge branch 'main' of ssh://git.eigen.co.id:2222/eigen/pos-realtime-report
continuous-integration/drone/tag Build is passing Details
2024-10-04 11:27:46 +07:00
Firman Ramdhani 4c6c52d59b feat: fix undefined user name 2024-10-04 11:27:43 +07:00
Supan Adit Pratama c77059971c ci: update try use caddy
continuous-integration/drone/tag Build is passing Details
2024-10-04 04:18:08 +00:00
Supan Adit Pratama 45178d1312 ci: change use npm
continuous-integration/drone/tag Build is passing Details
2024-10-04 02:48:53 +00:00
shancheas 13758c5730 fix: missing dependencies 2024-10-04 09:43:11 +07:00
26 changed files with 1516 additions and 366 deletions

View File

@ -20,6 +20,40 @@ steps:
build_args: build_args:
- env_target=env.production-online - env_target=env.production-online
- release_version=${DRONE_TAG} - release_version=${DRONE_TAG}
- name: kustomize-production
image: registry.k8s.io/kustomize/kustomize:v5.0.0
environment:
DEVOPS_SSH_PRIVATE:
from_secret: DEVOPS_SSH_PRIVATE
DEVOPS_SSH_PUBLIC:
from_secret: DEVOPS_SSH_PUBLIC
commands:
- mkdir -p ~/.ssh &&
- echo $DEVOPS_SSH_PRIVATE | base64 -d > ~/.ssh/id_rsa &&
- echo $DEVOPS_SSH_PUBLIC | base64 -d > ~/.ssh/id_rsa.pub &&
- ssh-keyscan -H -p 2222 git.eigen.co.id >> ~/.ssh/known_hosts &&
- chmod 700 ~/.ssh/ &&
- chmod 600 ~/.ssh/id_rsa &&
- git clone ssh://git@git.eigen.co.id:2222/eigen/k8s-kustomize-external.git &&
- cd k8s-kustomize-external/weplay-pos-production
- kustomize edit set image registry.eigen.co.id/eigen/$DRONE_REPO_NAME-production-online=registry.eigen.co.id/eigen/$DRONE_REPO_NAME-production-online:$DRONE_TAG &&
- kustomize edit set image registry.eigen.co.id/eigen/$DRONE_REPO_NAME-production-offline=registry.eigen.co.id/eigen/$DRONE_REPO_NAME-production-offline:$DRONE_TAG &&
- git add . &&
- |-
git commit -m "feat: update $DRONE_REPO_NAME production to $DRONE_TAG" &&
- git push origin master
- name: send-message
image: plugins/webhook
settings:
urls: https://mattermost.eigen.co.id/api/v4/posts
content_type: application/json
headers:
- Authorization=Bearer 5zubexudb38uuradfa36qy98ca
template: |
{
"channel_id": "s1ekqde1c3du5p35g6budnuotc",
"message": "Build {{repo.name}} sudah selesai"
}
trigger: trigger:
ref: ref:
- refs/tags/*-production.* - refs/tags/*-production.*

View File

@ -1,4 +1,4 @@
FROM node:20-alpine as build FROM node:18-alpine as build
ARG env_target ARG env_target
ARG release_version ARG release_version
@ -12,14 +12,9 @@ COPY env/$env_target /app/.env
RUN echo -e "\n" >> /app/.env RUN echo -e "\n" >> /app/.env
RUN echo -e "APP_VERSION=${release_version}" >> /app/.env RUN echo -e "APP_VERSION=${release_version}" >> /app/.env
RUN yarn install RUN npm install
RUN npm run build
RUN yarn build FROM caddy:2.6.1-alpine
COPY caddy/Caddyfile /etc/caddy/Caddyfile
FROM nginx:1.16.0-alpine COPY --from=build /app/dist /srv
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

5
caddy/Caddyfile Normal file
View File

@ -0,0 +1,5 @@
:80 {
root * /srv
try_files {path} /index.html
file_server
}

6
env/env.cloud vendored Normal file
View File

@ -0,0 +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|superadmin

3
env/env.development vendored
View File

@ -1,3 +1,6 @@
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

6
env/env.development-cloud vendored Normal file
View File

@ -0,0 +1,6 @@
VITE_APP_MODE=production
VITE_BASE_API_URL=https://api.sky.cloud.eigen.co.id/api
VITE_BASE_API_REPORT_URL=https://api.sky.cloud.eigen.co.id/api
VITE_BASE_API_URL_LOCAL=https://api.sky.cloud.eigen.co.id/api
VITE_BASE_ACCESS_SETTING=Endy|dev

View File

@ -1,3 +1,6 @@
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,3 +1,6 @@
VITE_APP_MODE=production VITE_APP_MODE=production
VITE_BASE_API_URL=https://api.office.weplayground.id/api VITE_BASE_API_URL=http://103.187.147.241:30050/api
VITE_BASE_API_REPORT_URL=https://api.office.weplayground.id/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

View File

@ -1,17 +0,0 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

46
package-lock.json generated
View File

@ -8,6 +8,9 @@
"name": "pos-realtime-report", "name": "pos-realtime-report",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"ag-grid-community": "^31.3.2",
"ag-grid-enterprise": "^31.3.2",
"ag-grid-react": "^31.3.2",
"antd": "^5.21.2", "antd": "^5.21.2",
"axios": "^1.7.7", "axios": "^1.7.7",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
@ -1823,6 +1826,38 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
}, },
"node_modules/ag-charts-community": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/ag-charts-community/-/ag-charts-community-9.3.2.tgz",
"integrity": "sha512-jw2llxTYzGAZ24m7eQsKS24BnJBhspZKsL03DbqH0wxLepbEcC3eeWICe+02TBQCbFVsWmSsYukjzQg3FkVWRw=="
},
"node_modules/ag-grid-community": {
"version": "31.3.4",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.3.4.tgz",
"integrity": "sha512-jOxQO86C6eLnk1GdP24HB6aqaouFzMWizgfUwNY5MnetiWzz9ZaAmOGSnW/XBvdjXvC5Fpk3gSbvVKKQ7h9kBw=="
},
"node_modules/ag-grid-enterprise": {
"version": "31.3.4",
"resolved": "https://registry.npmjs.org/ag-grid-enterprise/-/ag-grid-enterprise-31.3.4.tgz",
"integrity": "sha512-kreGRsFjz41APXXchLcQFtginnrmIGQYH48p7ydz33x8v+aja06HS5yEM6NP8j+VVHX43LeXnsl5Y4TLRgSoeg==",
"dependencies": {
"ag-charts-community": "9.3.2",
"ag-grid-community": "31.3.4"
}
},
"node_modules/ag-grid-react": {
"version": "31.3.4",
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-31.3.4.tgz",
"integrity": "sha512-WmPASHRFGSTxCMRStWG5bRtln0Ugsdqbb3+Y8sEyGHeLw4hXqfpqie3lT9kqCOl7wPWUjCpwmFdXzRnWPmyyeg==",
"dependencies": {
"ag-grid-community": "31.3.4",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -3521,7 +3556,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -3877,6 +3911,16 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",

View File

@ -10,6 +10,9 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"ag-grid-community": "^31.3.2",
"ag-grid-enterprise": "^31.3.2",
"ag-grid-react": "^31.3.2",
"antd": "^5.21.2", "antd": "^5.21.2",
"axios": "^1.7.7", "axios": "^1.7.7",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",

View File

@ -1,277 +1,21 @@
import axios from 'axios'; import { lazy } from 'react';
import AdminLayout from './layout'; import AdminLayout from './layout';
import { API_URL, currencyFormatter } from '@pos/base'; import { Navigate, Route, Routes } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Card, Col, DatePicker, notification, Row, Table, Typography } from 'antd';
import dayjs from 'dayjs';
import lodash, { filter } from 'lodash';
import { v4 } from 'uuid';
export default function Admin() { const ReportModule = lazy(() => import('./pages/report'));
const [dataItem, setDataItem] = useState<any[]>([]); const SettingModule = lazy(() => import('./pages/setting'));
const [dataItemKeys, setDataItemKeys] = useState<any[]>([]); const LogSettingModule = lazy(() => import('./pages/log-setting'));
const [loadingDataItem, setLoadingDataItem] = useState<boolean>(false);
const [dataItemTotalPax, setDataItemTotalPax] = useState<number>(0);
const [dataItemTotalRevenue, setDataItemTotalRevenue] = useState<number>(0);
const [dataItemMaster, setDataItemMaster] = useState<any[]>([]);
const [dataItemMasterKeys, setDataItemMasterKeys] = useState<any[]>([]);
const [loadingDataItemMaster, setLoadingDataItemMaster] = useState<boolean>(false);
const [dataItemMasterTotalPax, setDataItemMasterTotalPax] = useState<number>(0);
const [dataItemMasterTotalRevenue, setDataItemMasterTotalRevenue] = useState<number>(0);
const [filterDate, setFilerDate] = useState(dayjs());
async function getDataItem(params: any) {
setLoadingDataItem(true);
await axios
.get(API_URL.REPORT_SUMMARY_INCOME_ITEM, { params: params })
.then((resp) => {
const data = resp.data.data;
const groupedData = lodash(data)
.groupBy('item_owner') // Group by item_owner
.map((items, owner) => ({
// Map over each group to sum values and keep children
title: owner,
tr_item__qty: lodash.sumBy(items, (item) => Number(item.tr_item__qty)), // Convert to number
tr_item__total_net_price: lodash.sumBy(items, (item) => Number(item.tr_item__total_net_price)), // Convert to number
children: items.map((item) => {
return { ...item, title: item.tr_item__item_name };
}), // Include the original data as children
}))
.value()
.map((item) => {
return {
key: v4(),
...item,
};
});
const totalPax = lodash.sumBy(data, (item: any) => Number(item.tr_item__qty));
const totalRevenue = lodash.sumBy(data, (item: any) => Number(item.tr_item__total_net_price));
setDataItemTotalPax(totalPax);
setDataItemTotalRevenue(totalRevenue);
setDataItemKeys(groupedData.map((item) => item.key));
setDataItem(groupedData);
})
.catch((err) => {
notification.error({ message: err?.message });
})
.finally(() => {
setLoadingDataItem(false);
});
}
async function getDataItemMaster(params: any) {
setLoadingDataItemMaster(true);
await axios
.get(API_URL.REPORT_SUMMARY_INCOME_ITEM_MASTER, { params: params })
.then((resp) => {
const data = resp.data.data;
const groupedData = lodash(data)
.groupBy('item_owner') // Group by item_owner
.map((items, owner) => ({
// Map over each group to sum values and keep children
title: owner,
tr_item__qty: lodash.sumBy(items, (item) => Number(item.tr_item__qty)), // Convert to number
tr_item_bundling__total_net_price: lodash.sumBy(items, (item) =>
Number(item.tr_item_bundling__total_net_price),
), // Convert to number
children: items.map((item) => {
let title = '';
if (item.tr_item_bundling__item_name) {
title = `${item.tr_item_bundling__item_name} / ${item.tr_item__item_name}`;
} else {
title = item.tr_item__item_name;
}
return { ...item, title: title };
}), // Include the original data as children
}))
.value()
.map((item) => {
return {
key: v4(),
...item,
};
});
const totalPax = lodash.sumBy(data, (item: any) => Number(item.tr_item__qty));
const totalRevenue = lodash.sumBy(data, (item: any) => Number(item.tr_item_bundling__total_net_price));
setDataItemMasterTotalPax(totalPax);
setDataItemMasterTotalRevenue(totalRevenue);
setDataItemMasterKeys(groupedData.map((item) => item.key));
setDataItemMaster(groupedData);
})
.catch((err) => {
notification.error({ message: err?.message });
})
.finally(() => {
setLoadingDataItemMaster(false);
});
}
function handleGetDate(date: any) {
getDataItem({ date });
getDataItemMaster({ date });
}
useEffect(() => {
if (filterDate) {
handleGetDate(filterDate.format('DD-MM-YYYY'));
}
}, [filterDate]);
export default function AppModule() {
return ( return (
<AdminLayout> <AdminLayout>
<Row> <Routes>
<Col xl={8} lg={8} md={12} span={24}> <Route path="/report" element={<ReportModule />} />
<DatePicker <Route path="/setting" element={<SettingModule />} />
size="large" <Route path="/log-setting" element={<LogSettingModule />} />
popupStyle={{ fontSize: 16 }} <Route path="/" element={<Navigate to="/app/report" />} />
allowClear={false} <Route path="*" element={<Navigate to={'/404'} replace={true} />} />
value={filterDate} </Routes>
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

@ -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>
);
}

View File

@ -1,11 +1,14 @@
import { Avatar, Flex, Image, Layout, Popconfirm, Tooltip } from 'antd'; import { App, Avatar, Dropdown, Flex, Image, Layout } from 'antd';
import { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import { Content, Header } from 'antd/es/layout/layout'; import { Content, Header } from 'antd/es/layout/layout';
import { API_URL, getInitialName, handleLogout, UserDataState } from '@pos/base'; import { ACCESS_SETTING, API_URL, getInitialName, handleLogout, UserDataState } from '@pos/base';
import { FaUser } from 'react-icons/fa'; import { FaUser } from 'react-icons/fa';
import axios from 'axios'; import axios from 'axios';
import Logo from '../../../base/presentation/assets/images/we-logo.png'; import Logo from '../../../base/presentation/assets/images/we-logo.png';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import LocalDataConfiguration from './components/local-data-configuration';
import { FileProtectOutlined, FileTextOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
interface AdminLayoutProps { interface AdminLayoutProps {
children: ReactNode; children: ReactNode;
@ -13,21 +16,46 @@ interface AdminLayoutProps {
export default function AdminLayout(props: AdminLayoutProps) { export default function AdminLayout(props: AdminLayoutProps) {
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 { modal } = App.useApp();
const navigate = useNavigate();
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) {
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() {
navigate('/app/setting');
}
function gotoLogSetting() {
navigate('/app/log-setting');
}
return ( return (
<Layout> <Layout>
<Header <Header
@ -38,35 +66,88 @@ 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} /> <Image src={Logo} width={50} preview={false} onClick={gotoHome} style={{ cursor: 'pointer' }} />
{/* <div style={{ fontWeight: 800, fontSize: 16 }}>WE POS</div> */}
</Flex> </Flex>
<Flex style={{ marginLeft: 'auto' }}> <Flex style={{ marginLeft: 'auto' }} gap={15} align="center">
<Popconfirm <Dropdown
title="Are you sure want to logout?" trigger={['click']}
placement="bottomRight" overlayStyle={{ width: 150 }}
onConfirm={() => handleClickLogout()} 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: 'Log Setting',
icon: <FileProtectOutlined />,
onClick: () => gotoLogSetting(),
},
{
type: 'divider',
key: 'divider_3',
},
{
key: '4',
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', '3', 'divider_1', 'divider_2', 'divider_3'].includes(item.key)) {
return undefined;
}
return item;
})
.filter(Boolean) as any,
}}
> >
<Tooltip title="Logout" placement="bottom"> <Avatar size={35} style={{ cursor: 'pointer' }}>
<Avatar size={35}>
{initialName ? ( {initialName ? (
<div style={{ fontSize: 13 }}>{initialName?.toUpperCase()}</div> <div style={{ fontSize: 13 }}>{initialName?.toUpperCase()}</div>
) : ( ) : (
<FaUser style={{ fontSize: 14 }} /> <FaUser style={{ fontSize: 14 }} />
)} )}
</Avatar> </Avatar>
</Tooltip> </Dropdown>
</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

@ -0,0 +1,215 @@
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>();
const [currentPercentage, setCurrentPercentage] = useState<number>();
const [errorDefaultPercentage, setErrorDefaultPercentage] = useState<string>();
const [errorCurrentPercentage, setErrorCurrentPercentage] = useState<string>();
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/transaction-setting/detail', { params: { date: dayjs().format('YYYY-MM-DD') } })
.then((resp) => {
const value = resp.data?.data?.value;
form.setFieldsValue({ value: value });
setCurrentPercentage(value);
setErrorCurrentPercentage(null as any);
})
.catch((err) => {
const message = err.message;
setCurrentPercentage(null as any);
setErrorCurrentPercentage(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);
setErrorDefaultPercentage(null as any);
})
.catch((err) => {
const message = err.message;
setDefaultPercentage(null as any);
setErrorDefaultPercentage(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}>
{defaultPercentage ? (
<>
<div
style={{ fontSize: 24, fontWeight: 600 }}
className={`${makeColorTextValue(defaultPercentage)}`}
>{`${defaultPercentage >= 0 ? `${defaultPercentage} %` : '-'}`}</div>
<Button type="link" onClick={onOpenModal}>
Edit
</Button>
</>
) : (
<div>
{errorDefaultPercentage ? (
<div className="text-red-500 bg-red-100 p-3" style={{ borderRadius: 6 }}>
<div className="italic font-semibold">{`Error:`}</div>
<div className="italic">{errorDefaultPercentage}</div>
</div>
) : (
<div>{`-`}</div>
)}
</div>
)}
</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}>
{currentPercentage ? (
<>
<div
style={{ fontSize: 24, fontWeight: 600 }}
className={`${makeColorTextValue(currentPercentage)}`}
>{`${currentPercentage >= 0 ? `${currentPercentage} %` : '-'}`}</div>
<Button type="link" onClick={handleGetDataActive}>
Reload
</Button>
</>
) : (
<div>
{errorCurrentPercentage ? (
<div className="text-red-500 bg-red-100 p-3" style={{ borderRadius: 6 }}>
<div className="italic font-semibold">{`Error:`}</div>
<div className="italic">{errorCurrentPercentage}</div>
</div>
) : (
<div>{`-`}</div>
)}
</div>
)}
</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>
);
}

View File

@ -0,0 +1,109 @@
import { Form, Flex, List, Pagination, DatePicker, Button } from 'antd';
import { Fragment, useEffect, useState } from 'react';
import { HistoryOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import axios from 'axios';
export default function IndexLog() {
const [formFilter] = Form.useForm();
const [data, setData] = useState<any[]>([]);
const [meta, setMeta] = useState<any>();
const [loading, setLoading] = useState(false);
const [filterData, setFilterData] = useState<any>();
const handleGetData = async (page: number, filter: any) => {
setLoading(true);
await axios
.get('v1/data-scheduling-log', {
params: { page: page, limit: 10, order_by: 'log_created_at', order_type: 'DESC', ...(filter ?? {}) },
})
.then((resp: any) => {
const data = resp?.data?.data ?? [];
const meta = resp?.data?.meta;
setMeta(meta);
setData(data);
setFilterData(filter);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
useEffect(() => {
handleGetData(1, {});
}, []);
const handleFilter = async () => {
const { filter_date } = await formFilter.validateFields();
// const schedule_date_from = filter_date[0] ? dayjs(filter_date[0]).format('YYYY-MM-DD') : undefined;
// const schedule_date_to = filter_date[1] ? dayjs(filter_date[1]).format('YYYY-MM-DD') : undefined;
// await handleGetData(1, { schedule_date_from, schedule_date_to });
const log_created_from =
filter_date && filter_date[0] ? dayjs(filter_date[0]).startOf('days').valueOf() : undefined;
const log_created_to = filter_date && filter_date[1] ? dayjs(filter_date[1]).endOf('days').valueOf() : undefined;
await handleGetData(1, { log_created_from, log_created_to });
};
const handleResetFilter = async () => {
await formFilter.resetFields();
await handleGetData(1, {});
};
return (
<div>
<div style={{ marginBottom: 20 }}></div>
<Form form={formFilter}>
<Flex gap={8}>
<Form.Item name="filter_date">
<DatePicker.RangePicker size="large" />
</Form.Item>
<Button size="large" disabled={loading} type="primary" onClick={handleFilter}>
Filter
</Button>
<Button size="large" disabled={loading} onClick={handleResetFilter}>
Reset
</Button>
</Flex>
</Form>
<List
bordered
style={{ maxHeight: '75vh', overflow: 'auto' }}
dataSource={data}
loading={loading}
renderItem={(item) => (
<List.Item key={item.id} style={{ padding: 10 }}>
<Flex vertical gap={8}>
<Flex gap={8} align="center">
<HistoryOutlined />
<div className="font-bold text-md">
{dayjs(Number(item.log_created_at)).format('DD-MM-YYYY, HH:mm:ss')}
</div>
</Flex>
<div dangerouslySetInnerHTML={{ __html: item.description }}></div>
</Flex>
</List.Item>
)}
/>
<div style={{ marginBottom: 10 }}></div>
{data?.length > 0 && (
<Fragment>
<Flex justify="end" className="mt-2">
<Pagination
current={meta?.currentPage}
total={meta?.totalItems}
pageSize={10}
onChange={async (page) => {
await handleGetData(page, filterData);
}}
/>
</Flex>
</Fragment>
)}
</div>
);
}

View File

@ -0,0 +1,428 @@
import dayjs from 'dayjs';
import axios from 'axios';
import { Fragment, useEffect, useState } from 'react';
import {
App,
Button,
Card,
Col,
DatePicker,
Divider,
Flex,
Form,
Input,
InputNumber,
List,
Modal,
notification,
Pagination,
Row,
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_to),
});
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_to && dayjs(formValues.schedule_date_to).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>
);
}

View File

@ -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]';
}
}

View File

@ -0,0 +1,32 @@
import { ACCESS_SETTING, UserDataState } from '@pos/base';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import IndexLog from './components/index-log';
export default function LogSetting() {
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 (
<div>
<IndexLog />
</div>
);
}

View File

@ -0,0 +1,280 @@
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) => {
return child.key ?? child?.id ?? child.tr_item__item_name ?? child?.title;
}}
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) => {
return child.key ?? child?.id ?? child.tr_item__item_name ?? child?.title;
}}
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

@ -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>
);
}

View File

@ -1,19 +1,19 @@
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import { RecoilRoot } from 'recoil'; 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 { 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 PrivateApp = lazy(() => import('./admin')); const AppModule = lazy(() => import('./admin/index'));
export default function App() { export default function App() {
return ( return (
<RecoilRoot> <RecoilRoot>
<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' }}>
@ -23,12 +23,13 @@ export default function App() {
> >
<Routes> <Routes>
<Route path="/auth/*" element={<AuthApp />} /> <Route path="/auth/*" element={<AuthApp />} />
<Route path="/app" element={<PrivateApp />} /> <Route path="/app/*" element={<AppModule />} />
<Route path="/404" element={<NotFoundPage />} /> <Route path="/404" element={<NotFoundPage />} />
<Route path="/403" element={<ForbiddenAccessPage />} /> <Route path="/403" element={<ForbiddenAccessPage />} />
<Route path="*" element={<Navigate to="/app" />} /> <Route path="*" element={<Navigate to="/app" />} />
</Routes> </Routes>
</Suspense> </Suspense>
</AntdApp>
</ConfigProvider> </ConfigProvider>
</RecoilRoot> </RecoilRoot>
); );

View File

@ -5,4 +5,7 @@ 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,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_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;

View File

@ -1 +0,0 @@
export * from './recoil-debug-observer/recoil-debug-observer';

View File

@ -1,21 +0,0 @@
import { APP_MODE } from '@pos/base/infrastructure/constants';
import { ReactNode, useEffect } from 'react';
import { useRecoilSnapshot } from 'recoil';
export function DebugObserver(): ReactNode {
const snapshot = useRecoilSnapshot();
useEffect(() => {
if (APP_MODE === 'development') {
console.debug(
'%c' + 'The following atoms were modified:',
'color:yellow; font-weight:bold; text-transform: uppercase;',
);
for (const node of snapshot.getNodes_UNSTABLE({ isModified: true })) {
console.debug('%c' + node.key, 'color:white; background-color:blue', snapshot.getLoadable(node));
}
console.debug('%c' + '----------------------------------------', 'color:white');
}
}, [snapshot]);
return null;
}