diff --git a/env/env.development b/env/env.development
new file mode 100644
index 0000000..e9c0373
--- /dev/null
+++ b/env/env.development
@@ -0,0 +1,3 @@
+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
diff --git a/env/env.production b/env/env.production
new file mode 100644
index 0000000..2c6687b
--- /dev/null
+++ b/env/env.production
@@ -0,0 +1,3 @@
+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
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index afe48ac..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
-
-function App() {
- const [count, setCount] = useState(0)
-
- return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.tsx
and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
- )
-}
-
-export default App
diff --git a/src/apps/admin/index.tsx b/src/apps/admin/index.tsx
new file mode 100644
index 0000000..c5eec21
--- /dev/null
+++ b/src/apps/admin/index.tsx
@@ -0,0 +1,21 @@
+import { Navigate, Route, Routes } from 'react-router-dom';
+import AdminLayout from './layout';
+import { StorageDefaultURL } from '@pos/base';
+
+function DefaultUrl() {
+ const path: any = StorageDefaultURL.get();
+ return ;
+}
+
+export default function Admin() {
+ return (
+
+
+ item} />
+ item master} />
+ } />
+ } />
+
+
+ );
+}
diff --git a/src/apps/admin/layout/index.tsx b/src/apps/admin/layout/index.tsx
new file mode 100644
index 0000000..fe3dccf
--- /dev/null
+++ b/src/apps/admin/layout/index.tsx
@@ -0,0 +1,24 @@
+import { Layout } from 'antd';
+import { ReactNode } from 'react';
+import { Content } from 'antd/es/layout/layout';
+
+interface AdminLayoutProps {
+ children: ReactNode;
+}
+
+export default function AdminLayout(props: AdminLayoutProps) {
+ const { children } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/apps/auth/index.tsx b/src/apps/auth/index.tsx
new file mode 100644
index 0000000..ac1eab4
--- /dev/null
+++ b/src/apps/auth/index.tsx
@@ -0,0 +1,37 @@
+import { StorageActiveAccount } from '@pos/base';
+import React from 'react';
+import { useEffect } from 'react';
+import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
+
+const AuthLayout = React.lazy(() => import('./layout'));
+const Login = React.lazy(() => import('./pages/login'));
+
+export default function Auth() {
+ const navigate = useNavigate();
+
+ async function checkAccount() {
+ const activeAccount = await StorageActiveAccount.get();
+ if (activeAccount) navigate('/app', { replace: true });
+ }
+
+ useEffect(() => {
+ checkAccount();
+ }, []);
+
+ return (
+
+
+
+ } />
+ } />
+ } />
+
+
+
+ //
+ //
HELLO
+ //
Welcome Team!
+ //
{`Smile... it's a beautiful day.`}
+ //
+ );
+}
diff --git a/src/apps/auth/layout/index.tsx b/src/apps/auth/layout/index.tsx
new file mode 100644
index 0000000..76514a9
--- /dev/null
+++ b/src/apps/auth/layout/index.tsx
@@ -0,0 +1,17 @@
+import { Layout } from 'antd';
+import { ReactNode } from 'react';
+import { Content } from 'antd/es/layout/layout';
+
+interface AdminLayoutProps {
+ children: ReactNode;
+}
+
+export default function AuthLayout(props: AdminLayoutProps) {
+ const { children } = props;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/apps/auth/pages/login/components/index.ts b/src/apps/auth/pages/login/components/index.ts
new file mode 100644
index 0000000..ab8f2b0
--- /dev/null
+++ b/src/apps/auth/pages/login/components/index.ts
@@ -0,0 +1 @@
+export * from './login-form';
diff --git a/src/apps/auth/pages/login/components/login-form.less b/src/apps/auth/pages/login/components/login-form.less
new file mode 100644
index 0000000..dbadbe7
--- /dev/null
+++ b/src/apps/auth/pages/login/components/login-form.less
@@ -0,0 +1,38 @@
+.login-form {
+ width: 100vw;
+ height: 100vh;
+ background-color: #ebf7eb;
+ background-image: url('../../../../../base/presentation/assets/images/login-bg.png');
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: bottom center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ h1,
+ h2,
+ h3 {
+ margin: 0;
+ color: #5a0383;
+ }
+
+ h1,
+ h2 {
+ font-size: 2.5rem;
+ line-height: 2.7rem;
+ }
+
+ h1 {
+ font-weight: 100;
+ }
+
+ h2 {
+ font-weight: 900;
+ }
+
+ h3 {
+ font-weight: 200;
+ font-size: 1.5rem;
+ }
+}
diff --git a/src/apps/auth/pages/login/components/login-form.tsx b/src/apps/auth/pages/login/components/login-form.tsx
new file mode 100644
index 0000000..6d96d5e
--- /dev/null
+++ b/src/apps/auth/pages/login/components/login-form.tsx
@@ -0,0 +1,99 @@
+import { Button, Card, Form, Input, Popover } from 'antd';
+import './login-form.less';
+import { BsPersonCircle } from 'react-icons/bs';
+import { IoLockClosed } from 'react-icons/io5';
+import { useState } from 'react';
+import { Rule } from 'antd/es/form';
+import { capitalizeEachWord } from '@pos/base';
+
+interface Payload {
+ username: string;
+ password: string;
+}
+
+interface LoginForm {
+ onSubmit(payload: Payload): Promise;
+}
+
+function inputRule(label: string): Rule[] {
+ return [
+ { required: true, message: `${capitalizeEachWord(label)} harus diisi!` },
+ {
+ max: 100,
+ message: `${capitalizeEachWord(label)} maksimal 50 karakter!`,
+ },
+ ...(label === 'username'
+ ? [
+ {
+ pattern: new RegExp(/^[a-zA-Z0-9 ]*$/i),
+ message: `${capitalizeEachWord(label)} hanya menerima alfanumerik!`,
+ },
+ ]
+ : []),
+ ];
+}
+
+export function LoginForm(props: LoginForm) {
+ const onSubmit = props?.onSubmit;
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+
+ async function onFinish(payload: Payload) {
+ setLoading(true);
+ try {
+ if (onSubmit)
+ await onSubmit({
+ username: payload?.username?.trim(),
+ password: payload?.password?.trim(),
+ });
+ } catch ({ response }: any) {
+ form.setFields([{ name: 'validation', errors: [response?.data?.message] }]);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function onValuesChange() {
+ form.setFields([{ name: 'validation', errors: [] }]);
+ }
+
+ return (
+
+
+
+
Hello
+ Welcome Team!
+ Smile ... It's a beatiful day.
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/apps/auth/pages/login/index.tsx b/src/apps/auth/pages/login/index.tsx
new file mode 100644
index 0000000..7318d4d
--- /dev/null
+++ b/src/apps/auth/pages/login/index.tsx
@@ -0,0 +1,38 @@
+import { API_URL, decryptData, handleLogin, useQueryParam, WEB_URL } from '@pos/base';
+import { LoginForm } from './components';
+import axios from 'axios';
+import { useNavigate } from 'react-router-dom';
+
+const customAxios = axios.create();
+
+export default function LoginPage() {
+ const queryParam = useQueryParam();
+ const navigate = useNavigate();
+
+ async function handleSubmitLogin(payload: any) {
+ const { status, data } = await customAxios({
+ url: API_URL.LOGIN,
+ method: 'POST',
+ data: payload,
+ });
+ if (status === 201) {
+ const user = data?.data;
+ const role = user?.role;
+
+ await handleLogin(user);
+ if (role === 'tenant') {
+ navigate(WEB_URL.REPORT_TENANT);
+ } else {
+ if (queryParam.get('u')) {
+ const encPrevUrl = decryptData(decodeURIComponent(queryParam.get('u') as string));
+ if (encPrevUrl && encPrevUrl !== '' && !encPrevUrl?.includes('undefined')) {
+ const prevUrl = JSON.parse(encPrevUrl);
+ navigate(prevUrl, { replace: true });
+ } else navigate('/app', { replace: true });
+ } else navigate('/app', { replace: true });
+ }
+ }
+ }
+
+ return ;
+}
diff --git a/src/apps/index.tsx b/src/apps/index.tsx
new file mode 100644
index 0000000..d860177
--- /dev/null
+++ b/src/apps/index.tsx
@@ -0,0 +1,35 @@
+import { Suspense, lazy } from 'react';
+import { RecoilRoot } from 'recoil';
+import { ConfigProvider, Flex, Spin } 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'));
+
+export default function App() {
+ return (
+
+
+
+
+ } />
+
+ }
+ >
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ );
+}
diff --git a/src/assets/react.svg b/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/base/index.ts b/src/base/index.ts
index 3bf75bc..34f89db 100644
--- a/src/base/index.ts
+++ b/src/base/index.ts
@@ -13,6 +13,7 @@ export * from './infrastructure/constants';
export * from './infrastructure/helpers';
// Presentations
+export * from './presentation/components';
export * from './presentation/hooks';
export * from './presentation/providers';
export * from './presentation/states';
diff --git a/src/base/presentation/components/icon-wrapper/index.tsx b/src/base/presentation/components/icon-wrapper/index.tsx
new file mode 100644
index 0000000..214505f
--- /dev/null
+++ b/src/base/presentation/components/icon-wrapper/index.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+export function IconWrapper({ children }: { children?: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/base/presentation/components/index.ts b/src/base/presentation/components/index.ts
new file mode 100644
index 0000000..a6e5696
--- /dev/null
+++ b/src/base/presentation/components/index.ts
@@ -0,0 +1,2 @@
+export * from './icon-wrapper';
+export * from './page-setup';
diff --git a/src/base/presentation/components/page-setup/coming-soon/index.tsx b/src/base/presentation/components/page-setup/coming-soon/index.tsx
new file mode 100644
index 0000000..e603b65
--- /dev/null
+++ b/src/base/presentation/components/page-setup/coming-soon/index.tsx
@@ -0,0 +1,9 @@
+import image from '@pos/assets/images/coming-soon.jpg';
+
+export function ComingSoonPage() {
+ return (
+
+

+
+ );
+}
diff --git a/src/base/presentation/components/page-setup/forbidden-access/index.tsx b/src/base/presentation/components/page-setup/forbidden-access/index.tsx
new file mode 100644
index 0000000..abcda6b
--- /dev/null
+++ b/src/base/presentation/components/page-setup/forbidden-access/index.tsx
@@ -0,0 +1,54 @@
+// import { Button } from 'antd';
+// import { IconWrapper } from '../../icon-wrapper';
+// import { HiArrowLeft } from 'react-icons/hi2';
+// import { useNavigate } from 'react-router-dom';
+import { useAppModule } from '@pos/base/presentation/hooks';
+import Image4040 from '../../../assets/images/404.svg';
+import { Button } from 'antd';
+import { IconWrapper } from '@pos/base';
+import { HiArrowLeft } from 'react-icons/hi';
+import { useNavigate } from 'react-router-dom';
+
+export function ForbiddenAccessPage() {
+ const moduleAction = useAppModule();
+ const webUrl = moduleAction?.webUrl;
+
+ const navigate = useNavigate();
+
+ function handleBack() {
+ navigate(`${webUrl}`);
+ }
+
+ return (
+
+
+
+
Forbidden Access
+
+ Sorry, you do not have access to this page. Here are some helpful links:
+
+
+
+ {webUrl && (
+
+ )}
+
+
+
+
+

+
+
+
+ );
+}
diff --git a/src/base/presentation/components/page-setup/index.ts b/src/base/presentation/components/page-setup/index.ts
new file mode 100644
index 0000000..406785a
--- /dev/null
+++ b/src/base/presentation/components/page-setup/index.ts
@@ -0,0 +1,3 @@
+export * from './forbidden-access';
+export * from './not-found';
+export * from './coming-soon';
diff --git a/src/base/presentation/components/page-setup/not-found/index.tsx b/src/base/presentation/components/page-setup/not-found/index.tsx
new file mode 100644
index 0000000..8696fda
--- /dev/null
+++ b/src/base/presentation/components/page-setup/not-found/index.tsx
@@ -0,0 +1,44 @@
+import { Button } from 'antd';
+import { IconWrapper } from '../../icon-wrapper';
+import { HiArrowLeft } from 'react-icons/hi2';
+import { useNavigate } from 'react-router-dom';
+import Image4040 from '../../../assets/images/404.svg';
+
+export function NotFoundPage() {
+ const navigate = useNavigate();
+
+ function handleBack() {
+ navigate(-1);
+ }
+
+ return (
+
+
+
+
Page not found
+
+ Sorry, the page you are looking for doesn't exist. Here are some helpful links:
+
+
+
+
+
+
+
+
+

+
+
+
+ );
+}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 6119ad9..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,68 +0,0 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/src/main.tsx b/src/main.tsx
index 6f4ac9b..a3ca77f 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,21 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.tsx'
-import './index.css'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './apps';
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
+//Utils
+import './utils/axios-interceptor.ts';
+import './utils/credential-checker.ts';
+import './utils/g-license.ts';
+
+// Styles
+import '@pos/assets/styles/main.less';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+ ,
+);
diff --git a/src/utils/authorization.ts b/src/utils/authorization.ts
new file mode 100644
index 0000000..6e29135
--- /dev/null
+++ b/src/utils/authorization.ts
@@ -0,0 +1,3 @@
+//SAMPLE AUTHORIZATION TOKEN
+export const authorization =
+ 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImM1OWY4MTFlLTg3M2MtNDQ3Mi1iZDU4LTIxYzExMTkwMjExNCIsIm5hbWUiOiJzdXBlcmFkbWluIiwidXNlcm5hbWUiOiJzdXBlcmFkbWluIiwicm9sZSI6InN1cGVyYWRtaW4iLCJ1c2VyX3ByaXZpbGVnZV9pZCI6bnVsbCwiaWF0IjoxNzE3NjQ0MDAzLCJleHAiOjE3MTc2ODcyMDN9.nLwtLfAaHNTNRk6NMgGlYeybu1wIxW9WUWWeYChZUaw';
diff --git a/src/utils/axios-interceptor.ts b/src/utils/axios-interceptor.ts
new file mode 100644
index 0000000..bc759af
--- /dev/null
+++ b/src/utils/axios-interceptor.ts
@@ -0,0 +1,45 @@
+import axios, { AxiosError, AxiosResponse } from 'axios';
+import {
+ BASE_API_URL,
+ ErrorRequest,
+ StorageAccessToken,
+ handleLogout,
+ // StorageAccessToken
+} from '@pos/base';
+
+axios.defaults.baseURL = BASE_API_URL;
+axios.defaults.headers.post['Content-Type'] = 'application/json';
+axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
+axios.defaults.headers.post['Accept'] = '*/*';
+
+axios.interceptors.request.use(async (request: any) => {
+ const token = await StorageAccessToken.get();
+ // const token = authorization;
+ if (token) {
+ request.headers.authorization = token;
+ }
+
+ return request;
+});
+
+axios.interceptors.response.use(
+ (response) => {
+ const data = response.data;
+ const status = response.status;
+ return { data, status } as any;
+ },
+ (e) => {
+ const errorStatus = e.response?.status;
+ if (errorStatus === 401) {
+ const [, currentPath] = window.location.href.split('app/');
+ handleLogout('/app/' + currentPath);
+ }
+
+ const error = e as AxiosError;
+ const errorResponse: AxiosResponse = error?.response as AxiosResponse;
+ const errorData = errorResponse?.data;
+ const status = errorResponse?.status;
+ const message = errorData?.message ?? errorData;
+ return Promise.reject(new ErrorRequest(errorData, message, status));
+ },
+);
diff --git a/src/utils/credential-checker.ts b/src/utils/credential-checker.ts
new file mode 100644
index 0000000..c878c48
--- /dev/null
+++ b/src/utils/credential-checker.ts
@@ -0,0 +1,25 @@
+import dayjs from 'dayjs';
+import { API_URL, BASE_API_URL, handleLogout, StorageAccessToken } from '@pos/base';
+import axios from 'axios';
+
+async function credentialChecker() {
+ const token = await StorageAccessToken.get();
+ console.log(token);
+ if (token) {
+ const [, body] = token.split('.');
+ const bodyToken = JSON.parse(window.atob(body));
+ const expDate = bodyToken.exp;
+ const currentDate = dayjs().unix();
+ const isExpired = currentDate > expDate;
+ if (isExpired) {
+ const [, tokenBody] = token.split(' ');
+ const customAxios = axios.create();
+ await customAxios.post(BASE_API_URL + API_URL.FORCE_LOGOUT, { token: tokenBody });
+
+ const [, currentPath] = window.location.href.split('app/');
+ handleLogout('/app/' + currentPath);
+ }
+ }
+}
+
+credentialChecker();
diff --git a/src/utils/g-license.ts b/src/utils/g-license.ts
new file mode 100644
index 0000000..cd07f7a
--- /dev/null
+++ b/src/utils/g-license.ts
@@ -0,0 +1,6 @@
+import { LicenseManager } from 'ag-grid-enterprise';
+
+LicenseManager.prototype.isDisplayWatermark = () => false;
+LicenseManager.prototype.validateLicense = () => {
+ // Hehe boys
+};