├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo.png ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.test.tsx ├── assets │ └── icons │ │ ├── ic_google.svg │ │ ├── logo.png │ │ └── logo.svg ├── components │ ├── buttons │ │ ├── ButtonIcon.tsx │ │ └── ButtonPrimary.tsx │ ├── cards │ │ ├── Card.tsx │ │ ├── CardSkelton.tsx │ │ └── CardTransaction.tsx │ ├── forms │ │ └── Login.form.tsx │ ├── index.tsx │ ├── layouts │ │ ├── Auth.container.tsx │ │ ├── Login.container.tsx │ │ └── ScreenContainer.container.tsx │ ├── navbar │ │ └── Navbar.tsx │ ├── print │ │ ├── ComponentToPrint.tsx │ │ ├── PrintStruct.tsx │ │ └── styles.css │ ├── table │ │ └── TableItems.tsx │ └── tabs │ │ └── BottomTabs.tsx ├── global.d.ts ├── hooks │ └── index.ts ├── index.tsx ├── pages │ ├── App.tsx │ ├── index.tsx │ ├── login │ │ └── index.tsx │ ├── sales │ │ └── index.tsx │ └── transaction │ │ └── index.tsx ├── react-app-env.d.ts ├── redux │ ├── auth │ │ └── authSlice.ts │ ├── cart │ │ └── cartSlice.ts │ └── store.ts ├── reportWebVitals.ts ├── routes │ ├── PrivateRoute.tsx │ └── index.ts ├── service │ ├── api │ │ └── index.ts │ └── supabase │ │ ├── connection.ts │ │ └── transaction.ts ├── setupTests.ts ├── themes │ └── theme.ts ├── types │ ├── auth.type.ts │ ├── cart.type.ts │ └── transaction.type.ts └── utils │ ├── contstant.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_GOOGLE_CLIENT_ID= 2 | REACT_APP_GOOGLE_CLIENT_SECRET= 3 | REACT_APP_SUPABASE_URL= 4 | REACT_APP_SUPABASE_KEY= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/registerServiceWorker.js 2 | src/**/__tests__/** -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:react/recommended" 9 | ], 10 | "rules": { 11 | "@typescript-eslint/no-unused-vars": [ 12 | 2, 13 | { 14 | "vars": "all", 15 | "args": "all", 16 | "varsIgnorePattern": "^jsx$", 17 | "argsIgnorePattern": "[Ii]gnored$" 18 | } 19 | ], 20 | "@typescript-eslint/no-use-before-define": "off", 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off" 23 | }, 24 | "overrides": [ 25 | { 26 | // enable the rule specifically for TypeScript files 27 | "files": ["*.ts", "*.tsx"], 28 | "rules": { 29 | "@typescript-eslint/explicit-module-boundary-types": ["error"] 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | .env 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple POS Sesukamu 2 | 3 | A simple Point Of Sales application built using React & Typescript 4 | 5 | ## Getting Started 6 | 7 | To get started running the project locally, please follow the steps bello. 8 | 9 | First , clonse this repository 10 | 11 | ```bash 12 | git clone https://github.com/revell29/simple-pos-sesukamu.git 13 | ``` 14 | 15 | Then, install dependencies and fetch data to your local machine. **Note that we use Yarn, not npm. Make sure you using Node version 14 up.** 16 | 17 | ```bash 18 | cd simple-pos-sesukamu 19 | yarn install 20 | ``` 21 | 22 | Finally, run the development server.abs 23 | 24 | ```bash 25 | yarn start 26 | ``` 27 | 28 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 29 | 30 | ### Frequently Used Tools 31 | 32 | - [Typescript](https://www.typescriptlang.org/) 33 | - [Create-React-App](https://create-react-app.dev/docs/adding-typescript/) 34 | - [Chakra-UI](https://chakra-ui.com/) 35 | - [Measure Web Performance](https://web.dev/measure) 36 | - [Supabase.io](https://supabase.io) 37 | 38 | ### Features 39 | 40 | - Create Transaction & Show Transaction ✅ 41 | - Print Struct ✅ 42 | 43 | ## Notes 44 | 45 | This is still work in progress and you can contribute in this project 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pos-sesukamu", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/icons": "^1.0.15", 7 | "@chakra-ui/react": "^1.6.7", 8 | "@emotion/react": "^11", 9 | "@emotion/styled": "^11", 10 | "@reduxjs/toolkit": "^1.6.1", 11 | "@supabase/gotrue-js": "^1.18.0", 12 | "@supabase/supabase-js": "^1.22.6", 13 | "@testing-library/jest-dom": "^5.11.4", 14 | "@testing-library/react": "^11.1.0", 15 | "@testing-library/user-event": "^12.1.10", 16 | "@types/jest": "^26.0.15", 17 | "@types/node": "^12.0.0", 18 | "@types/react-dom": "^17.0.0", 19 | "@types/react-router-dom": "^5.1.9", 20 | "axios": "^0.21.4", 21 | "framer-motion": "^4", 22 | "react": "^17.0.2", 23 | "react-dom": "^17.0.2", 24 | "react-redux": "^7.2.5", 25 | "react-router-dom": "^5.3.0", 26 | "react-scripts": "4.0.3", 27 | "react-social-login": "^3.4.14", 28 | "react-spring-bottom-sheet": "^3.4.0", 29 | "react-to-print": "^2.13.0", 30 | "reactjs-social-login": "^1.4.2", 31 | "typescript": "^4.1.2", 32 | "web-vitals": "^1.0.1" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject", 39 | "lint:fix": "eslint './src/**/*.{ts,tsx}'" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "@types/react": "^17.0.21", 61 | "@typescript-eslint/eslint-plugin": "^4.31.1", 62 | "@typescript-eslint/parser": "^4.31.1", 63 | "eslint": "^7.32.0", 64 | "eslint-config-airbnb": "^18.2.1", 65 | "eslint-plugin-import": "^2.24.2", 66 | "eslint-plugin-jsx-a11y": "^6.4.1", 67 | "eslint-plugin-react": "^7.25.3", 68 | "eslint-plugin-react-hooks": "^4.2.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revell29/simple-pos-sesukamu/044b09d75bfb673dcbc09035e5356fa06b1b496a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Sesukamu T-Shirt Printing 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revell29/simple-pos-sesukamu/044b09d75bfb673dcbc09035e5356fa06b1b496a/public/logo.png -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revell29/simple-pos-sesukamu/044b09d75bfb673dcbc09035e5356fa06b1b496a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revell29/simple-pos-sesukamu/044b09d75bfb673dcbc09035e5356fa06b1b496a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Sesukamu POS", 3 | "name": "Point Of Sales Sesukamu", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import App from "./pages/App"; 4 | 5 | test("renders learn react link", () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/assets/icons/ic_google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revell29/simple-pos-sesukamu/044b09d75bfb673dcbc09035e5356fa06b1b496a/src/assets/icons/logo.png -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/buttons/ButtonIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, ButtonProps } from "@chakra-ui/react"; 3 | 4 | interface IButtonIcon extends ButtonProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | const ButtonIcon: React.FC = ({ 9 | children, 10 | ...rest 11 | }: IButtonIcon) => { 12 | return ( 13 | 16 | ); 17 | }; 18 | 19 | export default ButtonIcon; 20 | -------------------------------------------------------------------------------- /src/components/buttons/ButtonPrimary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, ButtonProps } from "@chakra-ui/react"; 3 | 4 | interface IButtonPrimary extends ButtonProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | const ButtonPrimary: React.FC = ({ 9 | children, 10 | ...rest 11 | }: IButtonPrimary) => { 12 | return ( 13 | 24 | ); 25 | }; 26 | 27 | export default ButtonPrimary; 28 | -------------------------------------------------------------------------------- /src/components/cards/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, BoxProps } from "@chakra-ui/react"; 3 | 4 | interface ICardProps extends BoxProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | const Card: React.FC = (props: ICardProps) => { 9 | const { children, ...rest } = props; 10 | 11 | return ( 12 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export default Card; 26 | -------------------------------------------------------------------------------- /src/components/cards/CardSkelton.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "components"; 2 | import React from "react"; 3 | import { SkeletonCircle, Skeleton, Flex } from "@chakra-ui/react"; 4 | 5 | const CardSkelton: React.FC = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default CardSkelton; 20 | -------------------------------------------------------------------------------- /src/components/cards/CardTransaction.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card } from "components"; 3 | import { Avatar, Flex, Heading, Box, Text, Button } from "@chakra-ui/react"; 4 | import { ITransaction } from "types/transaction.type"; 5 | import { currencyFormat, formatDate } from "utils"; 6 | import ReactToPrint from "react-to-print"; 7 | import ComponentToPrint from "components/print/ComponentToPrint"; 8 | import { useAppDispatch } from "hooks"; 9 | import { clearItems, printTransaction } from "redux/cart/cartSlice"; 10 | 11 | interface ICardTransactionProps { 12 | transaction: ITransaction; 13 | } 14 | 15 | const CardTransaction: React.FC = ( 16 | props: ICardTransactionProps 17 | ) => { 18 | const { transaction } = props; 19 | const componentRef = React.useRef(null); 20 | const dispatch = useAppDispatch(); 21 | 22 | const reactToPrintContent = React.useCallback(() => { 23 | return componentRef.current; 24 | }, [componentRef.current]); 25 | 26 | const printTrigger = React.useCallback(() => { 27 | return ( 28 | 31 | ); 32 | }, []); 33 | 34 | const handleOnBeforeGetContent = React.useCallback(() => { 35 | return new Promise((resolve) => { 36 | setTimeout(() => { 37 | dispatch(printTransaction(transaction)); 38 | resolve(); 39 | }, 2000); 40 | }); 41 | }, []); 42 | 43 | const handleAfterPrint = React.useCallback(() => { 44 | dispatch(clearItems()); 45 | }, [dispatch]); 46 | return ( 47 | 54 | 59 | 60 | 61 | 65 | 66 | 67 | {transaction.trx_number} 68 | 69 | {formatDate(transaction.transaction_date)} 70 | 71 | 72 | {transaction.customer_name} 73 | 74 | 75 | 76 | 77 | 84 | {currencyFormat(transaction.grand_total)} 85 | 86 | 87 | 93 | 94 | 95 | 96 | 97 | ); 98 | }; 99 | 100 | export default CardTransaction; 101 | -------------------------------------------------------------------------------- /src/components/forms/Login.form.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Image } from "@chakra-ui/react"; 3 | import { Card } from "components"; 4 | import { ButtonIcon } from "components"; 5 | import GoogleIcon from "assets/icons/ic_google.svg"; 6 | import { supabase } from "service/supabase/connection"; 7 | 8 | const LoginForm: React.FC = () => { 9 | const handleLogin = async () => { 10 | await supabase.auth.signIn( 11 | { 12 | provider: "google", 13 | }, 14 | { 15 | redirectTo: 16 | process.env.NODE_ENV === "development" 17 | ? "http://localhost:3000/sales" 18 | : "https://sesukamu-pos.vercel.app/sales", 19 | } 20 | ); 21 | }; 22 | 23 | return ( 24 | 25 | } 30 | > 31 | Masuk Ke Aplikasi 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default LoginForm; 38 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Card } from "./cards/Card"; 2 | export { default as LoginContainer } from "./layouts/Login.container"; 3 | export { default as LoginForm } from "./forms/Login.form"; 4 | export { default as ButtonPrimary } from "./buttons/ButtonPrimary"; 5 | export { default as ButtonIcon } from "./buttons/ButtonIcon"; 6 | export { default as AuthContainer } from "./layouts/Auth.container"; 7 | export { default as Navbar } from "./navbar/Navbar"; 8 | export { default as ScreenContainer } from "./layouts/ScreenContainer.container"; 9 | export { default as CardTransaction } from "./cards/CardTransaction"; 10 | export { default as CardSkelton } from "./cards/CardSkelton"; 11 | export { default as TableItem } from "./table/TableItems"; 12 | export { default as BottomTabs } from "./tabs/BottomTabs"; 13 | export { default as PrintStruct } from "./print/PrintStruct"; 14 | -------------------------------------------------------------------------------- /src/components/layouts/Auth.container.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "@chakra-ui/react"; 3 | import ApiRequest from "service/api"; 4 | import { useAppDispatch } from "hooks"; 5 | import { saveUserInfo } from "redux/auth/authSlice"; 6 | import { Navbar } from "components"; 7 | import { supabase } from "service/supabase/connection"; 8 | 9 | interface IAuthContainerProps { 10 | children: React.ReactNode; 11 | } 12 | 13 | const AuthContainer: React.FC = ({ 14 | children, 15 | }: IAuthContainerProps) => { 16 | const dispatch = useAppDispatch(); 17 | 18 | const getInfo = React.useCallback( 19 | async (token) => { 20 | await ApiRequest.getUserInfo(token) 21 | .then((res) => { 22 | dispatch(saveUserInfo(res)); 23 | }) 24 | .catch((err) => { 25 | throw err; 26 | }); 27 | }, 28 | [dispatch] 29 | ); 30 | 31 | React.useEffect(() => { 32 | const session = supabase.auth.session(); 33 | if (session) { 34 | getInfo(session?.provider_token); 35 | } 36 | }, []); 37 | 38 | return ( 39 | <> 40 | 41 | {children} 42 | 43 | ); 44 | }; 45 | 46 | export default AuthContainer; 47 | -------------------------------------------------------------------------------- /src/components/layouts/Login.container.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex } from "@chakra-ui/react"; 3 | 4 | type LoginContainerProps = { 5 | children: React.ReactNode; 6 | }; 7 | 8 | const LoginContainer: React.FC = ({ 9 | children, 10 | }: LoginContainerProps) => { 11 | return ( 12 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export default LoginContainer; 27 | -------------------------------------------------------------------------------- /src/components/layouts/ScreenContainer.container.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, BoxProps } from "@chakra-ui/react"; 3 | 4 | interface IScreenContainerProps extends BoxProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | const ScreenContainer: React.FC = ({ 9 | children, 10 | ...rest 11 | }: IScreenContainerProps) => { 12 | return ( 13 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default ScreenContainer; 25 | -------------------------------------------------------------------------------- /src/components/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Flex, 4 | Heading, 5 | Spacer, 6 | Image, 7 | Menu, 8 | MenuButton, 9 | MenuItem, 10 | MenuList, 11 | Text, 12 | } from "@chakra-ui/react"; 13 | import { useAppSelector } from "hooks"; 14 | import { supabase } from "service/supabase/connection"; 15 | 16 | const Navbar: React.FC = () => { 17 | const authInfo = useAppSelector((state) => state.authSlice); 18 | return ( 19 | 24 | 25 | 26 | Sesukamu 27 | 28 | 29 | 30 | 31 | 32 | {authInfo.name} 33 | 34 | 35 | 36 | 37 | 38 | 39 | Retur (Coming Soon) 40 | { 43 | supabase.auth.signOut(); 44 | window.location.href = "/"; 45 | }} 46 | > 47 | Sign Out 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default React.memo(Navbar); 57 | -------------------------------------------------------------------------------- /src/components/print/ComponentToPrint.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PrintStruct } from "components"; 3 | 4 | class ComponentToPrint extends React.Component { 5 | render(): JSX.Element { 6 | return ; 7 | } 8 | } 9 | 10 | export default ComponentToPrint; 11 | -------------------------------------------------------------------------------- /src/components/print/PrintStruct.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Image, Text, Flex } from "@chakra-ui/react"; 3 | import Logo from "assets/icons/logo.png"; 4 | import "./styles.css"; 5 | import { 6 | currencyFormat, 7 | formatDate, 8 | getGrandTotal, 9 | getTotalItems, 10 | } from "utils"; 11 | import { useAppSelector } from "hooks"; 12 | 13 | const Divider = () => ; 14 | 15 | const PrintStruct: React.FC = () => { 16 | const cartSlice = useAppSelector((state) => state.cartSlice); 17 | 18 | return ( 19 | 20 | 21 | 29 | Jl Bulak Jawa RT 003 RW 003 30 | Kel. Jatisari, Kec. Jatiasih 31 | Kota Bekasi 32 | +62 858-1716-6676 33 | 34 | 35 | No: {cartSlice.trx_number} 36 | Kasir: Syapirin 37 | Tanggal: {formatDate(cartSlice.transaction_date)} 38 | Customer: {cartSlice.customer_name} 39 | 40 | 41 | {cartSlice.items.map((item, index) => ( 42 | <> 43 | 44 | {item.item_name} 45 | 46 | 47 | {item.qty}x {currencyFormat(item.amount)} 48 | 49 | 50 | {currencyFormat(item.qty * item.amount)} 51 | 52 | 53 | 54 | 55 | 56 | ))} 57 | 58 | 59 | Subtotal 60 | 61 | {currencyFormat( 62 | getGrandTotal(cartSlice.items) * getTotalItems(cartSlice.items) 63 | )} 64 | 65 | 66 | 67 | 68 | TOTAL ({getTotalItems(cartSlice.items)} Barang) 69 | 70 | 71 | {currencyFormat( 72 | getGrandTotal(cartSlice.items) * getTotalItems(cartSlice.items) 73 | )} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 87 | 88 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | @kaos_sesukamu 100 | 101 | 102 | Terimakasih telah berbelanja ditoko kami. 103 | 104 | 105 | 106 | ); 107 | }; 108 | export default PrintStruct; 109 | -------------------------------------------------------------------------------- /src/components/print/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap"); 2 | 3 | @page { 4 | margin: 0; 5 | } 6 | 7 | @media print { 8 | .print { 9 | width: 264px; 10 | font-family: Courier Prime, monospace; 11 | margin: auto; 12 | font-weight: 500; 13 | font-size: 0.9rem; 14 | text-size-adjust: none; 15 | -webkit-text-size-adjust: none; 16 | -moz-text-size-adjust: none; 17 | -ms-text-size-adjust: none; 18 | } 19 | 20 | .hidden-print, 21 | .hidden-print * { 22 | display: none !important; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/table/TableItems.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Button, 4 | Box, 5 | Flex, 6 | Text, 7 | FormControl, 8 | Input, 9 | FormLabel, 10 | } from "@chakra-ui/react"; 11 | import { useAppSelector, useAppDispatch } from "hooks"; 12 | import { ItemCartType } from "types/cart.type"; 13 | import { BottomSheet } from "react-spring-bottom-sheet"; 14 | import "react-spring-bottom-sheet/dist/style.css"; 15 | import { currencyFormat, generateNumber } from "utils"; 16 | import { addItems, deleteItems } from "redux/cart/cartSlice"; 17 | import { Card } from "components"; 18 | import { CloseIcon } from "@chakra-ui/icons"; 19 | 20 | const TableItem: React.FC = () => { 21 | const initialState = { 22 | item_id: generateNumber(), 23 | item_name: "", 24 | qty: 0, 25 | amount: 0, 26 | }; 27 | const [open, setOpen] = React.useState(false); 28 | const dispatch = useAppDispatch(); 29 | const cartSlice = useAppSelector((state) => state.cartSlice); 30 | const [items, setItems] = React.useState(initialState); 31 | 32 | const addItem = () => { 33 | if (items.item_name !== "") { 34 | setOpen(false); 35 | dispatch(addItems(items)); 36 | setItems(initialState); 37 | } 38 | }; 39 | 40 | return ( 41 | <> 42 | 43 | 46 | 47 | 48 | {cartSlice.items.length > 0 && 49 | cartSlice.items.map((item, index) => ( 50 | 51 | 52 | 53 | 54 | {item.item_name} 55 | 56 | 57 | x{item.qty} 58 | 59 | 60 | 61 | 62 | {currencyFormat(item.amount)} 63 | 71 | 72 | 73 | 74 | 75 | ))} 76 | 77 | setOpen(false)}> 78 | 79 | 80 | Nama Item 81 | 83 | setItems({ ...items, item_name: e.target.value }) 84 | } 85 | value={items.item_name} 86 | /> 87 | 88 | 89 | Qty 90 | 93 | setItems({ ...items, qty: Number(e.target.value) }) 94 | } 95 | value={items.qty} 96 | /> 97 | 98 | 99 | Total 100 | 103 | setItems({ ...items, amount: Number(e.target.value) }) 104 | } 105 | value={items.amount} 106 | /> 107 | 108 | 111 | 112 | 113 | 114 | ); 115 | }; 116 | 117 | export default TableItem; 118 | -------------------------------------------------------------------------------- /src/components/tabs/BottomTabs.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from "@chakra-ui/react"; 2 | import React from "react"; 3 | import { currencyFormat } from "utils"; 4 | 5 | interface BottomTabsProps { 6 | total: number; 7 | } 8 | 9 | const BottomTabs: React.FC = (props: BottomTabsProps) => { 10 | const { total } = props; 11 | return ( 12 | 24 | 25 | 26 | Grand Total 27 | 28 | 29 | {currencyFormat(total)} 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default BottomTabs; 37 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "reactjs-social-login"; 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; 2 | import type { RootState, AppDispatch } from "redux/store"; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = (): any => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "pages/App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import { ChakraProvider } from "@chakra-ui/react"; 6 | import theme from "themes/theme"; 7 | import { store } from "redux/store"; 8 | import { Provider } from "react-redux"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals(); 25 | -------------------------------------------------------------------------------- /src/pages/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import PrivateRoute from "routes/PrivateRoute"; 4 | import { Login } from "."; 5 | 6 | const App: React.FC = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Login } from "./login"; 2 | export { default as SalesPage } from "./sales"; 3 | export { default as TransactionPage } from "./transaction"; 4 | -------------------------------------------------------------------------------- /src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LoginContainer, LoginForm } from "components"; 3 | import { Box, Heading } from "@chakra-ui/react"; 4 | import { useHistory } from "react-router"; 5 | import { supabase } from "service/supabase/connection"; 6 | 7 | const Login: React.FC = () => { 8 | const history = useHistory(); 9 | 10 | const checkToken = React.useCallback(() => { 11 | const session = supabase.auth.session(); 12 | if (session) { 13 | history.push("/sales"); 14 | } 15 | }, [history]); 16 | 17 | React.useEffect(() => { 18 | checkToken(); 19 | }, [checkToken]); 20 | 21 | return ( 22 | 23 | 24 | 25 | Sesukamu Printing 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Login; 34 | -------------------------------------------------------------------------------- /src/pages/sales/index.tsx: -------------------------------------------------------------------------------- 1 | import { CardTransaction, ScreenContainer, CardSkelton } from "components"; 2 | import React from "react"; 3 | import { RouteComponentProps } from "react-router-dom"; 4 | import { Flex, Heading, Button } from "@chakra-ui/react"; 5 | import { Link } from "react-router-dom"; 6 | import { AddIcon } from "@chakra-ui/icons"; 7 | import transaction from "service/supabase/transaction"; 8 | import { ITransaction } from "types/transaction.type"; 9 | 10 | const SalesPage: React.FC = () => { 11 | const [isLoading, setLoading] = React.useState(false); 12 | const [dataTransaction, setTransaction] = React.useState([]); 13 | 14 | const fetchTransaction = async () => { 15 | try { 16 | setLoading(true); 17 | const data: ITransaction[] = await transaction.fetchTransaction(); 18 | setTransaction(data); 19 | } catch (err) { 20 | console.log(err); 21 | } finally { 22 | setLoading(false); 23 | } 24 | }; 25 | 26 | React.useEffect(() => { 27 | fetchTransaction(); 28 | }, []); 29 | 30 | return ( 31 | <> 32 | 33 | 34 | 35 | Transaksi 36 | 37 | 49 | 50 | {!isLoading ? ( 51 | dataTransaction && 52 | dataTransaction.map( 53 | ( 54 | transaction: ITransaction, 55 | index: React.Key | null | undefined 56 | ) => 57 | ) 58 | ) : ( 59 | 60 | )} 61 | 62 | 63 | ); 64 | }; 65 | export default SalesPage; 66 | -------------------------------------------------------------------------------- /src/pages/transaction/index.tsx: -------------------------------------------------------------------------------- 1 | import { AddIcon, ChevronLeftIcon } from "@chakra-ui/icons"; 2 | import { 3 | Link, 4 | FormControl, 5 | FormLabel, 6 | Input, 7 | Flex, 8 | Button, 9 | } from "@chakra-ui/react"; 10 | import { BottomTabs, Card, ScreenContainer, TableItem } from "components"; 11 | import { Link as ReactLink, RouteComponentProps } from "react-router-dom"; 12 | import React from "react"; 13 | import transaction from "service/supabase/transaction"; 14 | import { dateCode, getGrandTotal, timeTransaction } from "utils"; 15 | import { useAppDispatch, useAppSelector } from "hooks"; 16 | import { useHistory } from "react-router-dom"; 17 | import { clearItems } from "redux/cart/cartSlice"; 18 | 19 | const TransactionPage: React.FC = () => { 20 | const [isLoading, setLoading] = React.useState(false); 21 | const [customer, setCustomer] = React.useState(""); 22 | const cartSlice = useAppSelector((state) => state.cartSlice); 23 | const grandTotal = getGrandTotal(cartSlice.items); 24 | const history = useHistory(); 25 | const dispatch = useAppDispatch(); 26 | 27 | const saveTransaction = async () => { 28 | try { 29 | setLoading(true); 30 | const payload = { 31 | trx_number: `SESUKAMU/${dateCode()}/${timeTransaction()}`, 32 | customer_name: customer, 33 | grand_total: getGrandTotal(cartSlice.items), 34 | is_paid: true, 35 | transaction_details: cartSlice.items.map((item) => { 36 | return { 37 | item_name: item.item_name, 38 | qty: item.qty, 39 | amount: item.amount, 40 | }; 41 | }), 42 | }; 43 | const data = await transaction.saveTransaction(payload); 44 | if (data) { 45 | history.push("/sales"); 46 | dispatch(clearItems()); 47 | } 48 | } catch (err) { 49 | console.log(err); 50 | } finally { 51 | setLoading(false); 52 | } 53 | }; 54 | 55 | return ( 56 | 57 | 58 | 64 | 65 | Kembali 66 | 67 | 81 | 82 | 83 | 84 | 85 | Nama Customer 86 | 87 | setCustomer(e.target.value)} 90 | value={customer} 91 | /> 92 | 93 | 94 | 95 | 96 | 97 | ); 98 | }; 99 | 100 | export default TransactionPage; 101 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/redux/auth/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { AuthSlice } from "types/auth.type"; 3 | 4 | const initialState: AuthSlice = { 5 | id: "", 6 | email: "", 7 | verified_email: null, 8 | name: "", 9 | given_name: "", 10 | family_name: "", 11 | picture: "", 12 | locale: "", 13 | }; 14 | 15 | export const authSlice = createSlice({ 16 | name: "auth-slice", 17 | initialState, 18 | reducers: { 19 | saveUserInfo: (state, action: PayloadAction) => { 20 | state.id = action.payload.id; 21 | state.email = action.payload.email; 22 | state.verified_email = action.payload.verified_email; 23 | state.name = action.payload.name; 24 | state.given_name = action.payload.given_name; 25 | state.family_name = action.payload.family_name; 26 | state.picture = action.payload.picture; 27 | state.locale = action.payload.locale; 28 | }, 29 | }, 30 | }); 31 | 32 | // Action creators are generated for each case reducer function 33 | export const { saveUserInfo } = authSlice.actions; 34 | 35 | export default authSlice.reducer; 36 | -------------------------------------------------------------------------------- /src/redux/cart/cartSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { CartSliceType, ItemCartType } from "types/cart.type"; 3 | import { ITransaction } from "types/transaction.type"; 4 | 5 | const initialState: CartSliceType = { 6 | customer_name: "", 7 | transaction_date: "", 8 | trx_number: "", 9 | grand_total: 0, 10 | is_paid: false, 11 | items: [], 12 | }; 13 | 14 | export const cartSlice = createSlice({ 15 | name: "cart-slsice", 16 | initialState, 17 | reducers: { 18 | addItems: (state, action: PayloadAction) => { 19 | const { payload } = action; 20 | const items = [...state.items, payload]; 21 | state.items = items; 22 | }, 23 | 24 | deleteItems: (state, action: PayloadAction) => { 25 | const { payload } = action; 26 | const items = state.items.filter((item) => item.item_id !== payload); 27 | state.items = items; 28 | }, 29 | clearItems: (state) => { 30 | state.customer_name = ""; 31 | state.transaction_date = ""; 32 | state.grand_total = 0; 33 | state.items = []; 34 | }, 35 | printTransaction: (state, action: PayloadAction) => { 36 | const { payload } = action; 37 | state.customer_name = payload.customer_name; 38 | state.transaction_date = payload.transaction_date; 39 | state.trx_number = payload.trx_number; 40 | state.grand_total = payload.grand_total; 41 | state.items = payload.transaction_details; 42 | }, 43 | }, 44 | }); 45 | 46 | export const { addItems, deleteItems, clearItems, printTransaction } = 47 | cartSlice.actions; 48 | export default cartSlice.reducer; 49 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import authSlice from "./auth/authSlice"; 3 | import cartSlice from "./cart/cartSlice"; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | authSlice, 8 | cartSlice, 9 | }, 10 | }); 11 | 12 | // Infer the `RootState` and `AppDispatch` types from the store itself 13 | export type RootState = ReturnType; 14 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 15 | export type AppDispatch = typeof store.dispatch; 16 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler): any => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/routes/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { AuthContainer } from "components"; 2 | import * as React from "react"; 3 | import { Switch, withRouter, Route } from "react-router-dom"; 4 | // import { supabase } from "service/supabase/connection"; 5 | import allRoutes from "."; 6 | 7 | const PrivateRoute = withRouter(() => { 8 | const AuthMenu = allRoutes.map((route, index) => ( 9 | } 14 | /> 15 | )); 16 | 17 | return ( 18 | 19 | {AuthMenu} 20 | 21 | ); 22 | }); 23 | 24 | export default PrivateRoute; 25 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { SalesPage, TransactionPage } from "pages"; 2 | 3 | const allRoutes = [ 4 | { 5 | path: "/sales", 6 | component: SalesPage, 7 | auth: true, 8 | }, 9 | { 10 | path: "/sales/transaction", 11 | component: TransactionPage, 12 | auth: true, 13 | }, 14 | ]; 15 | 16 | export default allRoutes; 17 | -------------------------------------------------------------------------------- /src/service/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { AuthSlice } from "types/auth.type"; 3 | 4 | export const ApiRequest = { 5 | getUserInfo: async (token: string | any): Promise => { 6 | try { 7 | const response = await axios.get( 8 | `https://www.googleapis.com/oauth2/v1/userinfo?access_token=${token}` 9 | ); 10 | return Promise.resolve(response.data); 11 | } catch (error) { 12 | return Promise.reject(error); 13 | } 14 | }, 15 | }; 16 | 17 | export default ApiRequest; 18 | -------------------------------------------------------------------------------- /src/service/supabase/connection.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | import { 3 | REACT_APP_SUPABASE_URL, 4 | REACT_APP_SUPABASE_KEY, 5 | } from "utils/contstant"; 6 | 7 | export const supabase = createClient( 8 | `${REACT_APP_SUPABASE_URL}`, 9 | `${REACT_APP_SUPABASE_KEY}` 10 | ); 11 | -------------------------------------------------------------------------------- /src/service/supabase/transaction.ts: -------------------------------------------------------------------------------- 1 | import { ITransaction } from "types/transaction.type"; 2 | import { supabase } from "./connection"; 3 | 4 | const transaction = { 5 | fetchTransaction: async (): Promise => { 6 | try { 7 | const { 8 | data: listTransaction, 9 | error, 10 | status, 11 | } = await supabase 12 | .from("transaction") 13 | .select( 14 | "customer_name, grand_total, trx_number, transaction_date, transaction_details(item_name, qty, amount)" 15 | ); 16 | 17 | if (error && status !== 406) { 18 | throw error; 19 | } 20 | 21 | return listTransaction; 22 | } catch (error: any) { 23 | return error; 24 | } 25 | }, 26 | saveTransaction: async (payload: ITransaction): Promise => { 27 | try { 28 | const { 29 | data: transaction, 30 | error, 31 | status, 32 | } = await supabase.from("transaction").insert({ 33 | trx_number: payload.trx_number, 34 | customer_name: payload.customer_name, 35 | is_paid: true, 36 | grand_total: payload.grand_total, 37 | }); 38 | 39 | if (error && status !== 406) { 40 | throw error; 41 | } 42 | 43 | if (transaction) { 44 | payload.transaction_details = payload.transaction_details.map( 45 | (item) => { 46 | return { 47 | ...item, 48 | transaction_id: transaction[0].transaction_id, 49 | }; 50 | } 51 | ); 52 | await supabase 53 | .from("transaction_details") 54 | .insert(payload.transaction_details); 55 | } 56 | 57 | return transaction; 58 | } catch (error) { 59 | return error; 60 | } 61 | }, 62 | }; 63 | 64 | export default transaction; 65 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/themes/theme.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme, theme as defaultTheme } from "@chakra-ui/react"; 2 | 3 | export default extendTheme({ 4 | colors: { 5 | primary: { 6 | 50: "#2447F9", 7 | }, 8 | }, 9 | shadows: { 10 | md: "0 5px 10px rgba(17, 17, 17, 0.075)", 11 | }, 12 | components: { 13 | Heading: { 14 | baseStyle: { 15 | fontWeight: 500, 16 | letterSpacing: "tighter", 17 | }, 18 | }, 19 | Button: { 20 | variants: { 21 | primary: { 22 | bg: "primary.50", 23 | color: "white", 24 | _hover: { 25 | boxShadow: "md", 26 | }, 27 | }, 28 | }, 29 | }, 30 | Link: { 31 | variants: { 32 | link: { 33 | color: "pink.400", 34 | }, 35 | }, 36 | }, 37 | Modal: { 38 | baseStyle: { 39 | overlay: { 40 | "@supports (backdrop-filter: blur(2px))": { 41 | backdropFilter: "blur(2px)", 42 | }, 43 | "@supports (-webkit-backdrop-filter: blur(2px))": { 44 | WebkitBackdropFilter: "blur(2px)", 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | 51 | fonts: { 52 | body: `'Archivo',${defaultTheme.fonts.body}`, 53 | heading: `'Manrope',${defaultTheme.fonts.heading}`, 54 | }, 55 | 56 | styles: { 57 | global: { 58 | html: { 59 | scrollBehavior: "smooth", 60 | }, 61 | "*:focus": { 62 | boxShadow: "none !important", 63 | }, 64 | body: { 65 | cursor: "default", 66 | fontFamily: "body", 67 | lineHeight: "base", 68 | background: "#F3F5F8", 69 | MozOsxFontSmoothing: "grayscale", 70 | WebkitFontSmoothing: "antialiased", 71 | textRendering: "optimizeLegibility", 72 | }, 73 | "*::placeholder": { 74 | color: "gray.200", 75 | }, 76 | "*, *::before, &::after": { 77 | // borderColor: "whiteAlpha.300", 78 | wordWrap: "break-word", 79 | }, 80 | 81 | "#nprogress": { 82 | pointerEvents: "none", 83 | }, 84 | "#nprogress .bar": { 85 | bgGradient: "linear(to-r, whiteAlpha.400, yellow.200)", 86 | h: "2px", 87 | left: 0, 88 | pos: "fixed", 89 | top: 0, 90 | w: "full", 91 | zIndex: 2000, 92 | }, 93 | ".nprogress-custom-parent": { 94 | overflow: "hidden", 95 | position: "absolute", 96 | }, 97 | ".background-left": { 98 | position: "fixed", 99 | bottom: "0px", 100 | left: 0, 101 | zIndex: -10, 102 | }, 103 | ".background-right": { 104 | position: "fixed", 105 | bottom: "0px", 106 | left: "2.5rem", 107 | zIndex: -10, 108 | }, 109 | }, 110 | }, 111 | }); 112 | -------------------------------------------------------------------------------- /src/types/auth.type.ts: -------------------------------------------------------------------------------- 1 | export interface AuthSlice { 2 | id: string; 3 | email: string; 4 | verified_email: boolean | null; 5 | name: string; 6 | given_name: string; 7 | family_name: string; 8 | picture: string; 9 | locale: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/cart.type.ts: -------------------------------------------------------------------------------- 1 | export interface ItemCartType { 2 | item_id?: string; 3 | item_name: string; 4 | qty: number; 5 | amount: number; 6 | } 7 | 8 | export interface CartSliceType { 9 | customer_name: string; 10 | transaction_date: any; 11 | trx_number: string; 12 | grand_total: number; 13 | is_paid: boolean; 14 | items: ItemCartType[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/types/transaction.type.ts: -------------------------------------------------------------------------------- 1 | import { ItemCartType } from "./cart.type"; 2 | 3 | export interface ITransaction { 4 | customer_name: string; 5 | grand_total: number; 6 | is_paid: boolean; 7 | transaction_date?: any; 8 | transaction_id?: number; 9 | trx_number: string; 10 | transaction_details: Array; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/contstant.ts: -------------------------------------------------------------------------------- 1 | export const REACT_APP_SUPABASE_URL = process.env.REACT_APP_SUPABASE_URL; 2 | export const REACT_APP_SUPABASE_KEY = process.env.REACT_APP_SUPABASE_KEY; 3 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { ItemCartType } from "types/cart.type"; 2 | 3 | export function currencyFormat(num = 0): string { 4 | const amount = Number(num); 5 | return "Rp " + amount.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,"); 6 | } 7 | 8 | /** 9 | * get grand_total from cart 10 | * 11 | * @param cart 12 | * @returns 13 | */ 14 | export function getGrandTotal(cart: Array): number { 15 | const result = cart.reduce( 16 | (prevValue, currentValue) => prevValue + currentValue.amount, 17 | 0 18 | ); 19 | return result; 20 | } 21 | 22 | /** 23 | * get total items in cart 24 | * @param cart 25 | * @returns 26 | */ 27 | export function getTotalItems(cart: Array): number { 28 | const result = cart.reduce( 29 | (prevValue, currentValue) => prevValue + currentValue.qty, 30 | 0 31 | ); 32 | return result; 33 | } 34 | 35 | /** 36 | * generate number for transaction number 37 | * @returns 38 | */ 39 | export function generateNumber(): string { 40 | const trxNumber = Date.now() + (Math.random() * 100).toFixed(); 41 | return trxNumber; 42 | } 43 | 44 | /** 45 | * formating timestamptz 46 | * 47 | * @param UTC_Date 48 | * @returns 49 | */ 50 | export const formatDate = (UTC_Date: string): string => { 51 | const localDate = new Date(UTC_Date); 52 | const objToday = localDate, 53 | dayOfMonth = 54 | objToday.getDate() < 10 ? "0" + objToday.getDate() : objToday.getDate(), 55 | curMonth = 56 | objToday.getMonth() + 1 < 10 57 | ? "0" + (objToday.getMonth() + 1) 58 | : objToday.getMonth() + 1, 59 | curYear = objToday.getFullYear(), 60 | curHour = objToday.getHours(), 61 | curMinute = 62 | objToday.getMinutes() < 10 63 | ? "0" + objToday.getMinutes() 64 | : objToday.getMinutes(), 65 | curMeridiem = objToday.getHours() > 12 ? "PM" : "AM"; 66 | const today = 67 | curYear + 68 | "-" + 69 | curMonth + 70 | "-" + 71 | dayOfMonth + 72 | " " + 73 | curHour + 74 | ":" + 75 | curMinute + 76 | " " + 77 | curMeridiem; 78 | return today; 79 | }; 80 | 81 | /** 82 | * generate date to transaction number 83 | * @returns 84 | */ 85 | export function dateCode(): string { 86 | const dateObj = new Date(); 87 | const monthDefault = dateObj.getUTCMonth() + 1; //months from 1-12 88 | const month = (monthDefault < 10 ? "0" : "") + monthDefault; 89 | const day = (dateObj.getUTCDate() < 10 ? "0" : "") + dateObj.getUTCDate(); 90 | const year = dateObj.getUTCFullYear(); 91 | const now = year + "" + month + "" + day; 92 | return now; 93 | } 94 | 95 | export function timeTransaction(): string { 96 | const dateObj = new Date(); 97 | const hours = (dateObj.getHours() < 10 ? "0" : "") + dateObj.getHours(); 98 | const minutes = (dateObj.getMinutes() < 10 ? "0" : "") + dateObj.getMinutes(); 99 | const second = (dateObj.getSeconds() < 10 ? "0" : "") + dateObj.getSeconds(); 100 | const time = hours + "" + minutes + "" + second; 101 | return time; 102 | } 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "src" 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------