diff --git a/.babelrc b/.babelrc index 88d5636caa8cf001e28beda10214d2590af8e0b0..733881f5a194e81daa78232ede104fc89e6ed842 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,18 @@ { - "presets": ["@babel/preset-react", "@babel/preset-env"] + "presets": ["@babel/preset-react", "@babel/preset-env", + [ + "@emotion/babel-preset-css-prop", + { + "sourceMap": false + } + ] + ], + "plugins": [ + [ + "transform-inline-environment-variables" + ], + [ + "emotion" + ] + ] } diff --git a/.eslintrc.json b/.eslintrc.json index ba6ab965317eb3fb546b0b96c73ac687dc39a9b8..eef303d6e6bdd98a064fff928795332719be6ea8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,10 +10,13 @@ "rules": { "react/prop-types": 0, "react-hooks/rules-of-hooks": "error", - "no-console": "warn" + "no-console": "warn", + "emotion/no-vanilla": "error", + "emotion/import-from-emotion": "error", + "emotion/styled-import": "error" }, "parser": "babel-eslint", - "plugins": ["react", "import", "jsx-a11y", "react-hooks"], + "plugins": ["react", "import", "jsx-a11y", "react-hooks", "emotion"], "parserOptions": { "ecmaVersion": 2019, "sourceType": "module", diff --git a/.gitignore b/.gitignore index 0d551e1e73e854b013491aff78c98b559b4beb60..a09068287042be8c9a3236d803d49d0319fa396b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .pnp.js /.cache /dist +.env # testing /coverage @@ -25,4 +26,5 @@ test-report.xml npm-debug.log* yarn-debug.log* yarn-error.log* -package-lock.json \ No newline at end of file +package-lock.json +/.vscode diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a5eeb839d19c3ce40e799b42ded08366481fa156..4d6afd1d03ad7665eb9800f0c97c8d431048aae2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -39,10 +39,11 @@ staging: variables: NETLIFY_AUTH_TOKEN: $STAGING_NETLIFY_AUTH_TOKEN NETLIFY_SITE_ID: $STAGING_NETLIFY_SITE_ID + REACT_APP_BASE_URL: $STAGING_REACT_APP_BASE_URL script: - npm install - CI=false npm run build - - npm install netlify-cli -g + - npm install netlify-cli -g --unsafe-perm - netlify deploy --dir dist --prod only: - staging @@ -54,6 +55,7 @@ production: AWS_ACCESS_KEY_ID: $PRODUCTION_AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY: $PRODUCTION_AWS_SECRET_ACCESS_KEY AWS_STORAGE_BUCKET_NAME: $PRODUCTION_AWS_STORAGE_BUCKET_NAME + REACT_APP_BASE_URL: $PRODUCTION_REACT_APP_BASE_URL script: - npm install - CI=false npm run build diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/README.md b/README.md index f7b548693144276ef5588674689a2ac8cf57069b..4a5fabb9b877030f5cc01b40a53da62850e0f5fe 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,38 @@ [](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-web/commits/master) [](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-web/commits/master) + +A local e-commerce application created for ease of buying and selling transactions. + +## Table of Contents +- [Install](#install) +- [Running Development Mode](#running-development-mode) +- [References](#references) + +## Install +The Admin Website's frontend uses Node.js and is developed using React. You need to install the required dependencies prior to building and contributing to the project. + +- [Node.js](https://nodejs.org/en/download/releases/) and npm package manager + +Verify that Node.js has been successfully installed. Make sure the interpreter can be invoked from the shell. For example: +``` +npm --version +``` + +Now install the packages required for Node.js: +``` +npm install +``` + +## Running Development Mode +To serve the frontend: +``` +npm run start +``` + +You can see the app running by going to localhost:1234 via your favourite web browser. + +## References +- https://docs.gitlab.com/ee/user/markdown.html#wiki---direct-page-link +- https://nodejs.org/en/download/package-manager/ + diff --git a/_redirects b/_redirects new file mode 100644 index 0000000000000000000000000000000000000000..7797f7c6a7356b0d451d11a49925df854c22e978 --- /dev/null +++ b/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/package.json b/package.json index ce9b1bd9cc6ca5f434aa92e7a7260b6eabff0b6e..27922000fd05ea2ec701d04c334e8b08febb09ad 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,28 @@ "version": "0.1.0", "private": true, "dependencies": { - "@reach/router": "^1.3.1", - "react": "^16.12.0", - "react-dom": "^16.12.0", - "react-scripts": "3.3.1" + "@emotion/babel-preset-css-prop": "^10.0.27", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@material-ui/core": "^4.9.11", + "@material-ui/icons": "^4.9.1", + "@reach/router": "^1.3.3", + "bootstrap": "^4.4.1", + "jquery": "^3.5.0", + "moment": "^2.24.0", + "moment-timezone": "^0.5.28", + "popper.js": "^1.16.1", + "react": "^16.13.1", + "react-bootstrap": "^1.0.1", + "react-dom": "^16.13.1", + "react-hook-form": "^5.6.0", + "react-moment": "^0.9.7", + "react-number-format": "^4.4.1", + "react-promise-tracker": "^2.1.0" }, "scripts": { "start": "parcel public/index.html", - "build": "parcel build public/index.html", + "build": "parcel build public/index.html --experimental-scope-hoisting", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", @@ -21,9 +35,9 @@ }, "browserslist": { "production": [ - ">0.2%", - "not dead", - "not op_mini all" + "last 3 chrome version", + "last 3 firefox version", + "last 3 safari version" ], "development": [ "last 1 chrome version", @@ -32,24 +46,40 @@ ] }, "devDependencies": { - "@babel/core": "^7.8.4", - "@babel/preset-env": "^7.8.4", - "@babel/preset-react": "^7.8.3", - "@testing-library/react": "^9.4.0", - "@types/jest": "^25.1.2", - "babel-eslint": "^10.0.3", + "@babel/core": "^7.9.0", + "@babel/preset-env": "^7.9.5", + "@babel/preset-react": "^7.9.4", + "@testing-library/dom": "^7.2.2", + "@testing-library/react": "^10.0.3", + "@types/jest": "^25.2.1", + "babel-eslint": "^10.1.0", + "babel-plugin-emotion": "^10.0.33", + "babel-plugin-transform-inline-environment-variables": "^0.4.3", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.2", "eslint": "^6.8.0", - "eslint-config-prettier": "^6.10.0", - "eslint-plugin-import": "^2.20.1", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-emotion": "^10.0.27", + "eslint-plugin-import": "^2.20.2", "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.18.3", - "eslint-plugin-react-hooks": "^2.3.0", - "jest": "^25.1.0", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-hooks": "^2.5.1", + "jest": "^25.4.0", + "jest-environment-enzyme": "^7.1.2", + "jest-enzyme": "^7.1.2", + "jest-fetch-mock": "^3.0.3", "jest-sonar-reporter": "^2.0.0", + "mutationobserver-shim": "^0.3.5", "parcel-bundler": "^1.12.4", - "prettier": "^1.19.1" + "prettier": "^2.0.5", + "regenerator-runtime": "^0.13.5" }, "jest": { + "setupFilesAfterEnv": [ + "jest-enzyme", + "<rootDir>/src/__test__/setup-jest.js" + ], + "testEnvironment": "enzyme", "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js", "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js" diff --git a/public/index.html b/public/index.html index c9919861e4383cd10ef97e10a28edab8cfdba0af..9303b31270f5841f1b09f241bdb1d7676b874f91 100644 --- a/public/index.html +++ b/public/index.html @@ -3,18 +3,13 @@ <head> <meta charset="utf-8" /> <link rel="icon" href="favicon.ico" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="logo192.png" /> - <!-- - manifest.json provides metadata used when your web app is installed on a - user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ - --> - <link rel="manifest" href="manifest.json" /> <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. @@ -26,7 +21,7 @@ --> <title>React App</title> </head> - <body> + <body style="background-color: #F5F8F9"> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <!-- diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c77ac21bb2ef88a6992b2b73ad93daaca92..0000000000000000000000000000000000000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index e9e57dc4d41b9b46e05112e9f45b7ea6ac0ba15e..0000000000000000000000000000000000000000 --- a/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/sonar-project.properties b/sonar-project.properties index e201ce2f9ed47169bb96a05e8d4e656116e8109b..9cc2b14329d57043aed8acf21b0e5d8f45ec2d76 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,3 +5,5 @@ sonar.testExecutionReportPaths=test-report.xml sonar.scm.provider=git sonar.sourceEncoding=UTF-8 sonar.sources=src +sonar.tests=src/__test__ +sonar.exclusions=**/*-jest.js \ No newline at end of file diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e053450a48a6bdb4d71aad648e7af821975c..0000000000000000000000000000000000000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.js b/src/App.js deleted file mode 100644 index 91ff38991c8bfbee985216ab3dbe0c12012e7811..0000000000000000000000000000000000000000 --- a/src/App.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import "./App.css"; - -const App = () => { - return ( - <div className="App"> - <header className="App-header"> - <p>Hello, World!</p> - </header> - </div> - ); -}; - -export default App; diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a5a6c442bd67ae20873206b6c9704f9f8f17e0de --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import ApplicationState from "./ApplicationState"; +import "bootstrap/dist/css/bootstrap.min.css"; + +const App = () => { + return <ApplicationState />; +}; + +export default App; diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index d4bea6c4efcba2c9242046c1a8aa0a85c76d8b56..0000000000000000000000000000000000000000 --- a/src/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import { render } from "@testing-library/react"; -import App from "./App"; - -test("renders hello world", () => { - const { getByText } = render(<App />); - const paragraphElement = getByText(/Hello, World!/i); - expect(paragraphElement.textContent).toStrictEqual("Hello, World!"); -}); diff --git a/src/ApplicationState.jsx b/src/ApplicationState.jsx new file mode 100644 index 0000000000000000000000000000000000000000..aaea2056fe7104eadfa23e30ad1303bf73cd9575 --- /dev/null +++ b/src/ApplicationState.jsx @@ -0,0 +1,37 @@ +import React, { useReducer } from "react"; +import AuthReducer, { initialState } from "./store/reducers/auth_reducer"; +import * as ACTIONS from "./store/actions/actions"; +import AuthContext from "./utils/contex"; +import Routes from "./routes"; +import Loader from "./component/Loader"; + +const ApplicationState = () => { + const [stateAuthReducer, dispatchAuthReducer] = useReducer( + AuthReducer, + initialState() + ); + + const handleLogin = (profile) => { + dispatchAuthReducer(ACTIONS.login(profile)); + }; + + const handleLogout = () => { + dispatchAuthReducer(ACTIONS.logout()); + }; + + return ( + <div> + <Loader /> + <AuthContext.Provider + value={{ + ...stateAuthReducer, + handleLogin, + handleLogout, + }} + > + <Routes /> + </AuthContext.Provider> + </div> + ); +}; +export default ApplicationState; diff --git a/src/__test__/App.test.js b/src/__test__/App.test.js new file mode 100644 index 0000000000000000000000000000000000000000..75cedc19ef81382f8b2cb0c1b2268b1a18ddca0d --- /dev/null +++ b/src/__test__/App.test.js @@ -0,0 +1,11 @@ +import React from "react"; +import { cleanup } from "@testing-library/react"; +import App from "../App"; +import { shallow } from "enzyme"; +import ApplicationState from "../ApplicationState"; + +afterEach(cleanup); +test("renders Application State", () => { + const wrapper = shallow(<App />); + expect(wrapper.find(ApplicationState)).toHaveLength(1); +}); diff --git a/src/__test__/DetailPengguna.test.js b/src/__test__/DetailPengguna.test.js new file mode 100644 index 0000000000000000000000000000000000000000..fe6bae921a659cf6c7a89fab42542cf6eeadd06a --- /dev/null +++ b/src/__test__/DetailPengguna.test.js @@ -0,0 +1,149 @@ +import React from "react"; +import { cleanup, render } from "@testing-library/react"; +import DetailPengguna from "../page/pengguna/DetailPengguna"; +import AuthContext from "../utils/contex"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("Test detail pengguna renders", async () => { + fetch + .once( + JSON.stringify({ + count: 17, + next: + "https://industripilar-staging.herokuapp.com/transactions/?page=2", + previous: null, + results: [ + { + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "whtestest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "TRF", + readable_payment_method: "Transfer", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: null, + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }, + ], + }) + ) + .once( + JSON.stringify({ + id: "663392ac-1dd6-462b-9301-a19c1287cefd", + username: "dummyuser", + full_name: "Dummy User", + phone_number: "+6285212345678", + address: "Jl. Dummy No.1", + neighborhood: "000", + hamlet: "000", + urban_village: "Dummy Urban Village", + sub_district: "Dummy Sub-District", + profile_picture: null, + }) + ); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailPengguna /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("profile")); + const profile = getByTestId("profile"); + expect(profile.textContent).toContain("dummyuser"); + expect(profile.textContent).toContain("+6285212345678"); + expect(profile.textContent).toContain("Jl. Dummy No.1"); + expect(profile.textContent).toContain("000"); + expect(profile.textContent).toContain("000"); + expect(profile.textContent).toContain("Dummy Sub-District"); + expect(profile.textContent).toContain("Dummy Urban Village"); +}); + +test("Test mock return error", async () => { + fetch + .once( + JSON.stringify({ + count: 17, + next: + "https://industripilar-staging.herokuapp.com/transactions/?page=2", + previous: null, + results: [ + { + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "whtestest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "TRF", + readable_payment_method: "Transfer", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: null, + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }, + ], + }) + ) + .mockReject(new Error("fake error message")); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailPengguna /> + </AuthContext.Provider> + ); + + const page = getByTestId("page-profile"); + await waitFor(() => + expect(page.textContent).toContain("Error !, Please relogin..") + ); +}); diff --git a/src/__test__/ListPengguna.test.js b/src/__test__/ListPengguna.test.js new file mode 100644 index 0000000000000000000000000000000000000000..05959e5fb3aaa15e7b26660f688497546b4288a8 --- /dev/null +++ b/src/__test__/ListPengguna.test.js @@ -0,0 +1,55 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import ListPengguna from "../page/pengguna/ListPengguna"; +import AuthContext from "../utils/contex"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("test fetching API", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + count: 2, + next: null, + previous: null, + results: [ + { + id: "45897cc5-968c-44cf-931d-e646b095fcaf", + username: "admin-staging", + full_name: "", + phone_number: "", + address: "", + neighborhood: "", + hamlet: "", + urban_village: "", + sub_district: "", + profile_picture: null, + }, + { + id: "663392ac-1dd6-462b-9301-a19c1287cefd", + username: "dummyuser", + full_name: "Dummy User", + phone_number: "+6285212345678", + address: "Jl. Dummy No.1", + neighborhood: "000", + hamlet: "000", + urban_village: "Dummy Urban Village", + sub_district: "Dummy Sub-District", + profile_picture: null, + }, + ], + }) + ); + + const { getByTestId, getByText } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <ListPengguna /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("tableList")); + const data = getByText("Dummy User"); + expect(data.textContent).toContain("Dummy User"); +}); diff --git a/src/__test__/Login.test.js b/src/__test__/Login.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e719999293c01b17476a4821f6566be0924701aa --- /dev/null +++ b/src/__test__/Login.test.js @@ -0,0 +1,67 @@ +import React from "react"; +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import Login from "../page/login/Login"; +import * as AuthContext from "../utils/contex"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("Test login page validation", async () => { + const handleLogin = jest.fn(); + jest + .spyOn(AuthContext, "useAuthContext") + .mockImplementation(() => ({ is_authenticated: true, handleLogin })); + const { getByTestId } = render(<Login />); + await act(async () => { + await fireEvent.submit(getByTestId("login")); + }); + expect(getByTestId("name-required").textContent).toEqual( + "Username field is required" + ); + expect(getByTestId("password-required").textContent).toEqual( + "Password field is required" + ); +}); + +test("Test login submitted", async () => { + const handleLogin = jest.fn(); + fetch.mockResponseOnce(JSON.stringify({ token: "AAAAAA" })); + jest + .spyOn(AuthContext, "useAuthContext") + .mockImplementation(() => ({ is_authenticated: true, handleLogin })); + + const { getByTestId } = render(<Login />); + + const username = getByTestId("name-input"); + const password = getByTestId("password-input"); + await act(async () => { + await fireEvent.input(username, { target: { value: "test" } }); + await fireEvent.input(password, { target: { value: "password" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("login")); + }); + expect(handleLogin).toBeCalled(); + expect(fetch).toBeCalled(); +}); + +test("Test login error", async () => { + fetch.mockResponseOnce([ + JSON.stringify([{ token: "AAAAAA" }]), + { status: 404 }, + ]); + const { getByTestId } = render(<Login />); + const username = getByTestId("name-input"); + const password = getByTestId("password-input"); + await act(async () => { + await fireEvent.input(username, { target: { value: "test" } }); + await fireEvent.input(password, { target: { value: "password" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("login")); + }); + const login = getByTestId("login"); + expect(login.textContent).toContain("Password salah !"); +}); diff --git a/src/__test__/ProtectedRoute.test.js b/src/__test__/ProtectedRoute.test.js new file mode 100644 index 0000000000000000000000000000000000000000..829664f3622de6f568226a584dcf69508fda7d68 --- /dev/null +++ b/src/__test__/ProtectedRoute.test.js @@ -0,0 +1,30 @@ +import React from "react"; +import { cleanup } from "@testing-library/react"; +import * as AuthContext from "../utils/contex"; +import ProtectedRoute from "../component/routes/ProtectedRoute"; +import { shallow } from "enzyme"; +import { Redirect } from "@reach/router"; +import Layout from "../layout/Layout"; +import ListProduk from "../page/produk/ListProduk"; + +afterEach(cleanup); + +test("Test protected route authenticated", () => { + jest + .spyOn(AuthContext, "useAuthContext") + .mockImplementation(() => ({ is_authenticated: true })); + const wrapper = shallow( + <ProtectedRoute path="/product" component={ListProduk} /> + ); + expect(wrapper.find(Layout).dive().find(ListProduk)).toHaveLength(1); +}); + +test("Test protected route unauthenticated", () => { + jest + .spyOn(AuthContext, "useAuthContext") + .mockImplementation(() => ({ is_authenticated: false })); + const wrapper = shallow( + <ProtectedRoute path="/product" component={ListProduk} /> + ); + expect(wrapper.find(Redirect)).toHaveLength(1); +}); diff --git a/src/__test__/UnauthenticatedRoute.test.js b/src/__test__/UnauthenticatedRoute.test.js new file mode 100644 index 0000000000000000000000000000000000000000..7f6965a0edf0ce2d608f78500c909837f7634155 --- /dev/null +++ b/src/__test__/UnauthenticatedRoute.test.js @@ -0,0 +1,24 @@ +import React from "react"; +import { cleanup } from "@testing-library/react"; +import * as AuthContext from "../utils/contex"; +import UnauthenticatedRoute from "../component/routes/UnauthenticatedRoute"; +import Login from "../page/login/Login"; +import { shallow } from "enzyme"; +import { Redirect } from "@reach/router"; +afterEach(cleanup); + +test("Test unauthenticated route authenticated", () => { + jest + .spyOn(AuthContext, "useAuthContext") + .mockImplementation(() => ({ is_authenticated: true })); + const wrapper = shallow(<UnauthenticatedRoute path="/" component={Login} />); + expect(wrapper.find(Redirect)).toHaveLength(1); +}); + +test("Test unauthenticated route unauthenticated", () => { + jest + .spyOn(AuthContext, "useAuthContext") + .mockImplementation(() => ({ is_authenticated: false })); + const wrapper = shallow(<UnauthenticatedRoute path="/" component={Login} />); + expect(wrapper.find(Login)).toHaveLength(1); +}); diff --git a/src/__test__/auth_reducer.test.js b/src/__test__/auth_reducer.test.js new file mode 100644 index 0000000000000000000000000000000000000000..369b834d32d24d235e5480829769ed7400f8042d --- /dev/null +++ b/src/__test__/auth_reducer.test.js @@ -0,0 +1,26 @@ +import { cleanup } from "@testing-library/react"; +import AuthReducer, { initialState } from "../store/reducers/auth_reducer"; +import * as ACTIONS from "../store/actions/actions"; + +afterEach(cleanup); + +test("auth reducer test", () => { + expect(AuthReducer(initialState(), {})).toEqual(initialState()); + + const profile = { + name: "test", + token: "abcd", + }; + const expectationLogin = { + is_authenticated: true, + profile: { ...profile }, + }; + const loginAction = ACTIONS.login(profile); + expect(AuthReducer(initialState(), loginAction)).toEqual(expectationLogin); + + const logoutAction = ACTIONS.logout(); + expect(AuthReducer(expectationLogin, logoutAction)).toEqual({ + is_authenticated: false, + profile: null, + }); +}); diff --git a/src/__test__/context_state.test.js b/src/__test__/context_state.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c2c988d630c100b2720d0dbca3316812637ed8f7 --- /dev/null +++ b/src/__test__/context_state.test.js @@ -0,0 +1,33 @@ +import React from "react"; +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import ApplicationState from "../ApplicationState"; +import { navigate } from "@reach/router"; + +afterEach(cleanup); + +test("Test context state", async () => { + fetch.mockResponseOnce(JSON.stringify({ token: "AAAAAA" })); + const { getByTestId } = render(<ApplicationState />); + const input = getByTestId("name-input"); + const name = "test"; + const password = getByTestId("password-input"); + await act(async () => { + await fireEvent.input(input, { target: { value: name } }); + await fireEvent.input(password, { target: { value: "password" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("login")); + }); + + fetch.mockResponseOnce(JSON.stringify({ results: [] })); + await act(async () => { + await navigate("/pengguna"); + }); + expect(getByTestId("page").textContent).toContain("Pengguna"); + + await act(async () => { + await fireEvent.click(getByTestId("logout")); + }); + await navigate("/"); + expect(getByTestId("login").tagName).toEqual("FORM"); +}); diff --git a/src/__test__/kategori/DetailKategori.test.js b/src/__test__/kategori/DetailKategori.test.js new file mode 100644 index 0000000000000000000000000000000000000000..dbc611cf8aa0f8409976e8de1e12a90173ee672a --- /dev/null +++ b/src/__test__/kategori/DetailKategori.test.js @@ -0,0 +1,283 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import DetailKategori from "../../page/kategori/DetailKategori"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("Test detail kategori renders", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }, + { + id: "f3fb1295-8420-4d00-ba5e-48579092551b", + name: "Kemeja", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "kemeja putih", + price: "200000.00", + stock: 5, + image: null, + }, + { + id: "45435428-e323-4f1f-8e07-f48312605504", + name: "celana panjang tidur", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + { + id: "0eb3cefe-4b70-4817-94fc-d91273cd5132", + name: "celana panjang tidur 1", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }) + ); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailKategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page-detail-kategori")); + const subkategori = getByTestId("page-detail-kategori"); + expect(subkategori.textContent).toContain("Baju"); + expect(subkategori.textContent).toContain("celana panjang tidur 1"); + expect(fetch.mock.calls.length).toEqual(2); +}); + +test("Test mock detail kategori return error", async () => { + fetch.mockReject(new Error("fake error message")); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailKategori /> + </AuthContext.Provider> + ); + const page = getByTestId("page-detail-kategori"); + await waitFor(() => + expect(page.textContent).toContain("Error !, Please relogin..") + ); +}); + +test("Test detail kategori delete", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }, + { + id: "f3fb1295-8420-4d00-ba5e-48579092551b", + name: "Kemeja", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "kemeja putih", + price: "200000.00", + stock: 5, + image: null, + }, + { + id: "45435428-e323-4f1f-8e07-f48312605504", + name: "celana panjang tidur", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + { + id: "0eb3cefe-4b70-4817-94fc-d91273cd5132", + name: "celana panjang tidur 1", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }) + ) + .once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailKategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page-detail-kategori")); + const subkategori = getByTestId("page-detail-kategori"); + expect(subkategori.textContent).toContain("Baju"); + expect(subkategori.textContent).toContain("celana panjang tidur 1"); + expect(fetch.mock.calls.length).toEqual(2); + const btnDeleteModal = getByTestId("button-delete-category-modal"); + await act(async () => { + await fireEvent.click(btnDeleteModal); + }); + const btnDelete = getByTestId("button-delete-category"); + await act(async () => { + await fireEvent.click(btnDelete); + }); + expect(fetch.mock.calls.length).toEqual(3); +}); + +test("Test detail kategori delete error", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }, + { + id: "f3fb1295-8420-4d00-ba5e-48579092551b", + name: "Kemeja", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "kemeja putih", + price: "200000.00", + stock: 5, + image: null, + }, + { + id: "45435428-e323-4f1f-8e07-f48312605504", + name: "celana panjang tidur", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + { + id: "0eb3cefe-4b70-4817-94fc-d91273cd5132", + name: "celana panjang tidur 1", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }) + ) + .mockReject(new Error("fake error message")); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailKategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page-detail-kategori")); + const subkategori = getByTestId("page-detail-kategori"); + expect(subkategori.textContent).toContain("Baju"); + expect(subkategori.textContent).toContain("celana panjang tidur 1"); + expect(fetch.mock.calls.length).toEqual(2); + const btnDeleteModal = getByTestId("button-delete-category-modal"); + await act(async () => { + await fireEvent.click(btnDeleteModal); + }); + const btnDelete = getByTestId("button-delete-category"); + await act(async () => { + await fireEvent.click(btnDelete); + }); + expect(subkategori.textContent).toContain( + "Tidak dapat menghapus kategori, mohon periksa apakah ada produk " + + "didalam kategori ini." + ); +}); diff --git a/src/__test__/kategori/EditKategori.test.js b/src/__test__/kategori/EditKategori.test.js new file mode 100644 index 0000000000000000000000000000000000000000..08ad8ac15d0a40c4e9066433dfa90d00b98f76d8 --- /dev/null +++ b/src/__test__/kategori/EditKategori.test.js @@ -0,0 +1,74 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import { waitFor } from "@testing-library/dom"; +import React from "react"; +import EditKategori from "../../page/kategori/EditKategori"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); +test("Test edit kategori renders", async () => { + fetch + .once( + JSON.stringify({ + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }) + ) + .once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <EditKategori /> + </AuthContext.Provider> + ); + const waitKategori = getByTestId("waiting-edit-kategori"); + expect(waitKategori.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("edit-kategori")); + const name_kategori = getByTestId("name-kategori-input"); + expect(name_kategori.value).toEqual("Baju"); + expect(fetch.mock.calls.length).toEqual(1); + await act(async () => { + await fireEvent.input(name_kategori, { target: { value: "test" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-category")); + }); + + expect(fetch.mock.calls.length).toEqual(2); +}); + +test("Test Edit kategori error", async () => { + fetch + .once( + JSON.stringify({ + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }) + ) + .once(JSON.stringify({}), { status: 400 }); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <EditKategori /> + </AuthContext.Provider> + ); + const waitKategori = getByTestId("waiting-edit-kategori"); + expect(waitKategori.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("edit-kategori")); + const name_kategori = getByTestId("name-kategori-input"); + expect(name_kategori.value).toEqual("Baju"); + expect(fetch.mock.calls.length).toEqual(1); + await act(async () => { + await fireEvent.input(name_kategori, { target: { value: "test" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-category")); + }); + const category = getByTestId("edit-kategori"); + expect(category.textContent).toContain("Error !, Data tidak dapat disimpan"); + expect(fetch.mock.calls.length).toEqual(2); +}); diff --git a/src/__test__/kategori/ListKategori.test.js b/src/__test__/kategori/ListKategori.test.js new file mode 100644 index 0000000000000000000000000000000000000000..18dd0f10a7b5d9dfb1439600c7d0abc2cb54516a --- /dev/null +++ b/src/__test__/kategori/ListKategori.test.js @@ -0,0 +1,51 @@ +import { cleanup, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import ListKategori from "../../page/kategori/ListKategori"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test(" Test List Kategori", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + count: 4, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "69faa710-fbe2-45f1-98e8-f80d3c4c1bbe", + name: "Celana", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + ], + }) + ); + const { getByTestId, getByText } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <ListKategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("tableList")); + const data = getByText("Baju"); + expect(data.textContent).toContain("Baju"); + expect(fetch.mock.calls.length).toEqual(1); +}); diff --git a/src/__test__/kategori/TambahKategori.test.js b/src/__test__/kategori/TambahKategori.test.js new file mode 100644 index 0000000000000000000000000000000000000000..fed5dc6ffd8f427400ed0d233811e6d4672c0c6f --- /dev/null +++ b/src/__test__/kategori/TambahKategori.test.js @@ -0,0 +1,60 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import TambahKategori from "../../page/kategori/TambahKategori"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); +test("Test tambah kategori renders", async () => { + fetch.once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahKategori /> + </AuthContext.Provider> + ); + const name_kategori = getByTestId("name-kategori-input"); + await act(async () => { + await fireEvent.input(name_kategori, { target: { value: "test" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-category")); + }); + expect(fetch.mock.calls.length).toEqual(1); +}); + +test("Test tambah kategori form required", async () => { + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahKategori /> + </AuthContext.Provider> + ); + await act(async () => { + await fireEvent.submit(getByTestId("submit-category")); + }); + const formCategory = getByTestId("form-category"); + expect(formCategory.textContent).toContain( + "Nama kategori tidak boleh kosong" + ); +}); + +test("Test tambah kategori error", async () => { + fetch.once(JSON.stringify({}), { status: 400 }); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahKategori /> + </AuthContext.Provider> + ); + const name_kategori = getByTestId("name-kategori-input"); + await act(async () => { + await fireEvent.input(name_kategori, { target: { value: "test" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-category")); + }); + const category = getByTestId("tambah-kategori"); + expect(category.textContent).toContain("Error !, Data tidak dapat disimpan"); + expect(fetch.mock.calls.length).toEqual(1); +}); diff --git a/src/__test__/produk/DetailProduk.test.js b/src/__test__/produk/DetailProduk.test.js new file mode 100644 index 0000000000000000000000000000000000000000..16b8313ba36578dc1218874ab1eee57097c89811 --- /dev/null +++ b/src/__test__/produk/DetailProduk.test.js @@ -0,0 +1,151 @@ +import { act, cleanup, render, fireEvent } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import DetailProduk from "../../page/produk/DetailProduk"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("Test detail produk renders", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Kue Nastar", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + description: "Kue Nastar enak maknyuss", + price: 70000, + stock: 5, + image: null, + pre_order: false, + }) + ); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailProduk /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("produk-detail")); + const produk = getByTestId("produk-detail"); + expect(produk.textContent).toContain("Kue Nastar"); + expect(produk.textContent).toContain("5"); + expect(fetch.mock.calls.length).toEqual(1); +}); + +test("Test detail produk renders preorder", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Kue Nastar", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + description: "Kue Nastar enak maknyuss", + price: 70000, + stock: null, + image: null, + pre_order: true, + }) + ); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailProduk /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("produk-detail")); + const produk = getByTestId("produk-detail"); + expect(produk.textContent).toContain("Kue Nastar"); + expect(produk.textContent).toContain("Preorder"); + expect(fetch.mock.calls.length).toEqual(1); +}); + +test("Test mock detail produk return error", async () => { + fetch.mockReject(new Error("fake error message")); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailProduk /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page")); + const page = getByTestId("page"); + expect(page.textContent).toContain("Error !, Please relogin.."); +}); + +test("Test detail produk delete", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }) + ); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailProduk /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page")); + const produk = getByTestId("page"); + expect(produk.textContent).toContain("Jas"); + expect(fetch.mock.calls.length).toEqual(1); + const btnDeleteModal = getByTestId("button-delete-product-modal"); + await act(async () => { + await fireEvent.click(btnDeleteModal); + }); + const btnDelete = getByTestId("button-delete-product"); + await act(async () => { + await fireEvent.click(btnDelete); + }); + expect(fetch.mock.calls.length).toEqual(2); +}); + +test("Test detail produk delete error", async () => { + fetch + .mockResponseOnce( + JSON.stringify({ + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }) + ) + .mockReject(new Error("fake error message")); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailProduk /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page")); + const produk = getByTestId("page"); + expect(produk.textContent).toContain("Jas"); + expect(fetch.mock.calls.length).toEqual(1); + const btnDeleteModal = getByTestId("button-delete-product-modal"); + await act(async () => { + await fireEvent.click(btnDeleteModal); + }); + const btnDelete = getByTestId("button-delete-product"); + await act(async () => { + await fireEvent.click(btnDelete); + }); + expect(fetch.mock.calls.length).toEqual(2); + expect(produk.textContent).toContain( + "Tidak dapat menghapus produk, mohon periksa apakah ada produk ini." + ); +}); diff --git a/src/__test__/produk/EditProduk.test.js b/src/__test__/produk/EditProduk.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f2851820394a3b5dd60186671e9bc79b57fcc32b --- /dev/null +++ b/src/__test__/produk/EditProduk.test.js @@ -0,0 +1,197 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import { waitFor } from "@testing-library/dom"; +import React from "react"; +import EditProduk from "../../page/produk/EditProduk"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); +test("Test edit produk renders", async () => { + fetch + .once( + JSON.stringify({ + id: "3d403cd3-e356-4c15-9a86-8843333e2778", + code: "5VK6TY", + name: "a", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur hehe", + description: "celana", + price: "50000.00", + stock: 17, + image: null, + pre_order: false, + }) + ) + .once( + JSON.stringify({ + count: 4, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + { + id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c", + name: "Minuman", + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + count: 7, + next: null, + previous: null, + results: [ + { + id: "626aa022-50a7-4d3a-b658-79cb0f059b03", + name: "Baju Tidur hehe", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + { + id: "ab222bb8-46e4-40bb-bd47-7f546d356de1", + name: "joker baru", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({}), { statusCode: 200 }); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <EditProduk /> + </AuthContext.Provider> + ); + const waitProduk = getByTestId("waiting-edit-produk"); + expect(waitProduk.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("edit-produk")); + const name_produk = getByTestId("name-produk-input"); + expect(name_produk.value).toEqual("a"); + await act(async () => { + await fireEvent.input(name_produk, { target: { value: "test" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-produk")); + }); + + expect(fetch.mock.calls.length).toEqual(5); +}); + +test("Test edit produk renders error", async () => { + fetch + .once( + JSON.stringify({ + id: "3d403cd3-e356-4c15-9a86-8843333e2778", + code: "5VK6TY", + name: "a", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur hehe", + description: "celana", + price: "50000.00", + stock: 17, + image: null, + pre_order: false, + }) + ) + .once( + JSON.stringify({ + count: 4, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + { + id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c", + name: "Minuman", + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + count: 7, + next: null, + previous: null, + results: [ + { + id: "626aa022-50a7-4d3a-b658-79cb0f059b03", + name: "Baju Tidur hehe", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + { + id: "ab222bb8-46e4-40bb-bd47-7f546d356de1", + name: "joker baru", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({}), { status: 400 }); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <EditProduk /> + </AuthContext.Provider> + ); + const waitProduk = getByTestId("waiting-edit-produk"); + expect(waitProduk.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("edit-produk")); + const name_produk = getByTestId("name-produk-input"); + expect(name_produk.value).toEqual("a"); + const stock_produk = getByTestId("stock-produk-input"); + await act(async () => { + await fireEvent.input(stock_produk, { target: { value: "-1" } }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-produk")); + }); + const produk = getByTestId("edit-produk"); + expect(produk.textContent).toContain( + "Error loading form !, Please relogin.." + ); + expect(fetch.mock.calls.length).toEqual(5); +}); diff --git a/src/__test__/produk/ListProduk.test.js b/src/__test__/produk/ListProduk.test.js new file mode 100644 index 0000000000000000000000000000000000000000..5a5094b884e594a6310d51361aa8f7ee66659e35 --- /dev/null +++ b/src/__test__/produk/ListProduk.test.js @@ -0,0 +1,57 @@ +import { cleanup, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import { waitFor } from "@testing-library/dom"; +import ListProduk from "../../page/produk/ListProduk"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test(" Test List produk", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + count: 23, + next: "https://industripilar-staging.herokuapp.com/products/?page=2", + previous: null, + results: [ + { + id: "3d403cd3-e356-4c15-9a86-8843333e2778", + code: "5VK6TY", + name: "a", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur hehe", + description: "celana", + price: "50000.00", + stock: 9, + image: null, + }, + { + id: "9a0bccaa-70f6-48a8-89fc-5c5994684729", + code: "4QKSBC", + name: "Piyama", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur hehe", + description: "piyama", + price: "50000.00", + stock: 14, + image: null, + }, + ], + }) + ); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "tester" } }}> + <ListProduk /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("tableList")); + const data = getByTestId("tableList"); + expect(data.textContent).toContain("Piyama"); + expect(fetch.mock.calls.length).toEqual(1); +}); diff --git a/src/__test__/produk/TambahProduk.test.js b/src/__test__/produk/TambahProduk.test.js new file mode 100644 index 0000000000000000000000000000000000000000..69d0828fdf638be530f5e34380ab6b653ec80f34 --- /dev/null +++ b/src/__test__/produk/TambahProduk.test.js @@ -0,0 +1,257 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import TambahProduk from "../../page/produk/TambahProduk"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); +test("Test tambah produk renders", async () => { + fetch + .once( + JSON.stringify({ + count: 4, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + { + id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c", + name: "Minuman", + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + count: 7, + next: null, + previous: null, + results: [ + { + id: "626aa022-50a7-4d3a-b658-79cb0f059b03", + name: "Baju Tidur hehe", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + { + id: "ab222bb8-46e4-40bb-bd47-7f546d356de1", + name: "joker baru", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({}), { statusCode: 200 }); + + const { getByTestId, getByLabelText } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahProduk /> + </AuthContext.Provider> + ); + const name_produk = getByTestId("name-produk-input"); + await act(async () => { + await fireEvent.input(name_produk, { target: { value: "test" } }); + }); + const desc_produk = getByTestId("desc-produk-input"); + await act(async () => { + await fireEvent.input(desc_produk, { target: { value: "test" } }); + }); + const price_produk = getByTestId("price-produk-input"); + await act(async () => { + await fireEvent.input(price_produk, { target: { value: "1" } }); + }); + const stock_produk = getByTestId("stock-produk-input"); + await act(async () => { + await fireEvent.input(stock_produk, { target: { value: "1" } }); + }); + await act(async () => { + await fireEvent.click(getByLabelText("Biasa"), { + target: { value: "false" }, + }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-produk")); + }); + expect(fetch.mock.calls.length).toEqual(4); +}); + +test("Test tambah produk form required", async () => { + fetch + .once( + JSON.stringify({ + count: 4, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + { + id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c", + name: "Minuman", + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + count: 7, + next: null, + previous: null, + results: [ + { + id: "626aa022-50a7-4d3a-b658-79cb0f059b03", + name: "Baju Tidur hehe", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + { + id: "ab222bb8-46e4-40bb-bd47-7f546d356de1", + name: "joker baru", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahProduk /> + </AuthContext.Provider> + ); + await act(async () => { + await fireEvent.submit(getByTestId("submit-produk")); + }); + const formCategory = getByTestId("form-produk"); + expect(formCategory.textContent).toContain("Nama Produk tidak boleh kosong"); + expect(fetch.mock.calls.length).toEqual(3); +}); + +test("Test tambah produk error", async () => { + fetch + .once( + JSON.stringify({ + count: 4, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + { + id: "b8a9909b-9131-4c2b-bcc5-6bceb18f702c", + name: "Minuman", + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + count: 7, + next: null, + previous: null, + results: [ + { + id: "626aa022-50a7-4d3a-b658-79cb0f059b03", + name: "Baju Tidur hehe", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + { + id: "ab222bb8-46e4-40bb-bd47-7f546d356de1", + name: "joker baru", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({}), { status: 400 }); + + const { getByTestId, getByLabelText } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahProduk /> + </AuthContext.Provider> + ); + const name_produk = getByTestId("name-produk-input"); + await act(async () => { + await fireEvent.input(name_produk, { target: { value: "test" } }); + }); + const desc_produk = getByTestId("desc-produk-input"); + await act(async () => { + await fireEvent.input(desc_produk, { target: { value: "test" } }); + }); + const price_produk = getByTestId("price-produk-input"); + await act(async () => { + await fireEvent.input(price_produk, { target: { value: "1" } }); + }); + const stock_produk = getByTestId("stock-produk-input"); + await act(async () => { + await fireEvent.input(stock_produk, { target: { value: "1" } }); + }); + await act(async () => { + await fireEvent.click(getByLabelText("Biasa"), { + target: { value: "false" }, + }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-produk")); + }); + const produk = getByTestId("tambah-produk"); + expect(produk.textContent).toContain( + "Error loading form !, Please relogin.." + ); + expect(fetch.mock.calls.length).toEqual(4); +}); diff --git a/src/__test__/program/ListProgram.test.js b/src/__test__/program/ListProgram.test.js new file mode 100644 index 0000000000000000000000000000000000000000..90a69fe1dad5bd906bba84c8525ee6086298d21e --- /dev/null +++ b/src/__test__/program/ListProgram.test.js @@ -0,0 +1,41 @@ +import { cleanup, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import ListProgram from "../../page/program/ListProgram"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test(" Test List Program", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + count: 1, + next: null, + previous: null, + results: [ + { + id: "282007ff-d0be-4d68-823e-03bde086ee79", + name: "Pilar Belajar", + description: "Pilar Belajar adalah program belajar untuk anak-anak.", + start_date_time: "2020-04-08T10:00:00+07:00", + end_date_time: "2020-04-09T10:00:00+07:00", + location: "Balai Warga Jatinegara Baru", + speaker: "Sherlock Holmes", + poster_image: null, + }, + ], + }) + ); + const { getByTestId, getByText } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <ListProgram /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("tableList")); + const data = getByText("Pilar Belajar"); + expect(data.textContent).toContain("Pilar Belajar"); + expect(fetch.mock.calls.length).toEqual(1); +}); diff --git a/src/__test__/routes.test.js b/src/__test__/routes.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1707c167461eb65b475ea253938d8fa3a6c30089 --- /dev/null +++ b/src/__test__/routes.test.js @@ -0,0 +1,12 @@ +import React from "react"; +import { cleanup } from "@testing-library/react"; +import { shallow } from "enzyme"; +import { Router } from "@reach/router"; +import Routes from "../routes"; + +afterEach(cleanup); + +test("renders routes", () => { + const wrapper = shallow(<Routes />); + expect(wrapper.find(Router)).toHaveLength(1); +}); diff --git a/src/__test__/setup-jest.js b/src/__test__/setup-jest.js new file mode 100644 index 0000000000000000000000000000000000000000..4a8fa00624bec7ab04d56ce5e133b158c545b943 --- /dev/null +++ b/src/__test__/setup-jest.js @@ -0,0 +1,3 @@ +import "mutationobserver-shim"; +import "regenerator-runtime"; +global.fetch = require("jest-fetch-mock"); diff --git a/src/__test__/subkategori/DetailSubCategory.test.js b/src/__test__/subkategori/DetailSubCategory.test.js new file mode 100644 index 0000000000000000000000000000000000000000..3621acc8bc60ba397ea9f8f27adfe879c41321c8 --- /dev/null +++ b/src/__test__/subkategori/DetailSubCategory.test.js @@ -0,0 +1,289 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import DetailSubkategori from "../../page/subkategori/DetailSubkategori"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("Test detail subkategori renders", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }, + { + id: "f3fb1295-8420-4d00-ba5e-48579092551b", + name: "Kemeja", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "kemeja putih", + price: "200000.00", + stock: 5, + image: null, + }, + { + id: "45435428-e323-4f1f-8e07-f48312605504", + name: "celana panjang tidur", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + { + id: "0eb3cefe-4b70-4817-94fc-d91273cd5132", + name: "celana panjang tidur 1", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + id: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + name: "Baju Pergi", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju Pergi", + image: null, + }) + ); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailSubkategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page-detail-subkategori")); + const subkategori = getByTestId("page-detail-subkategori"); + expect(subkategori.textContent).toContain("Baju Pergi"); + expect(subkategori.textContent).toContain("celana panjang tidur 1"); + expect(fetch.mock.calls.length).toEqual(2); +}); + +test("Test mock detail subkategori return error", async () => { + fetch.mockReject(new Error("fake error message")); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailSubkategori /> + </AuthContext.Provider> + ); + const page = getByTestId("page-detail-subkategori"); + await waitFor(() => + expect(page.textContent).toContain("Error !, Please relogin..") + ); +}); + +test("Test detail subkategori delete", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }, + { + id: "f3fb1295-8420-4d00-ba5e-48579092551b", + name: "Kemeja", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "kemeja putih", + price: "200000.00", + stock: 5, + image: null, + }, + { + id: "45435428-e323-4f1f-8e07-f48312605504", + name: "celana panjang tidur", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + { + id: "0eb3cefe-4b70-4817-94fc-d91273cd5132", + name: "celana panjang tidur 1", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + id: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + name: "Baju Pergi", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju Pergi", + image: null, + }) + ) + .once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailSubkategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page-detail-subkategori")); + const subkategori = getByTestId("page-detail-subkategori"); + expect(subkategori.textContent).toContain("Baju Pergi"); + expect(subkategori.textContent).toContain("celana panjang tidur 1"); + expect(fetch.mock.calls.length).toEqual(2); + const btnDeleteModal = getByTestId("button-delete-subcategory-modal"); + await act(async () => { + await fireEvent.click(btnDeleteModal); + }); + const btnDelete = getByTestId("button-delete-subcategory"); + await act(async () => { + await fireEvent.click(btnDelete); + }); + expect(fetch.mock.calls.length).toEqual(3); +}); + +test("Test detail subkategori delete error", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "ff0473d9-be63-497e-afc9-8610f57423d8", + name: "Jas", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "Jas hitam", + price: "800000.00", + stock: 5, + image: null, + }, + { + id: "f3fb1295-8420-4d00-ba5e-48579092551b", + name: "Kemeja", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + subcategory_name: "Baju Pergi", + description: "kemeja putih", + price: "200000.00", + stock: 5, + image: null, + }, + { + id: "45435428-e323-4f1f-8e07-f48312605504", + name: "celana panjang tidur", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + { + id: "0eb3cefe-4b70-4817-94fc-d91273cd5132", + name: "celana panjang tidur 1", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + subcategory: "626aa022-50a7-4d3a-b658-79cb0f059b03", + subcategory_name: "Baju Tidur", + description: "celana panjang hitam tidur", + price: "90000.00", + stock: 15, + image: null, + }, + ], + }) + ) + .once( + JSON.stringify({ + id: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + name: "Baju Pergi", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju Pergi", + image: null, + }) + ) + .mockReject(new Error("fake error message")); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <DetailSubkategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("page-detail-subkategori")); + const subkategori = getByTestId("page-detail-subkategori"); + expect(subkategori.textContent).toContain("Baju Pergi"); + expect(subkategori.textContent).toContain("celana panjang tidur 1"); + expect(fetch.mock.calls.length).toEqual(2); + const btnDeleteModal = getByTestId("button-delete-subcategory-modal"); + await act(async () => { + await fireEvent.click(btnDeleteModal); + }); + const btnDelete = getByTestId("button-delete-subcategory"); + await act(async () => { + await fireEvent.click(btnDelete); + }); + expect(subkategori.textContent).toContain( + "Tidak dapat menghapus subkategori, mohon periksa apakah ada produk " + + "didalam kategori ini." + ); +}); diff --git a/src/__test__/subkategori/EditSubCategory.test.js b/src/__test__/subkategori/EditSubCategory.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e6b63a724b88ef13b0cf5ee1bed4b8e00525d2f8 --- /dev/null +++ b/src/__test__/subkategori/EditSubCategory.test.js @@ -0,0 +1,149 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; + +import { waitFor } from "@testing-library/dom"; +import React from "react"; +import EditSubkategori from "../../page/subkategori/EditSubkategori"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); +test("Test edit subkategori renders", async () => { + fetch + .once( + JSON.stringify({ + id: "cbc3dcdd-98e2-459f-b244-6f07819dda8a", + name: "avengers endgame", + category: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + category_name: "Mainan", + image: + "https://industripilar-api-staging.s3.amazonaws.com/media/uploads/subcategories/1_txtcYocQEGtOFN33ZCTDbw.png", + }) + ) + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <EditSubkategori /> + </AuthContext.Provider> + ); + const waitSubkategori = getByTestId("waiting-edit-subkategori"); + expect(waitSubkategori.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("edit-subkategori")); + const name_subkategori = getByTestId("name-subkategori-input"); + expect(name_subkategori.value).toEqual("avengers endgame"); + await waitFor(() => getByTestId("category-subcategory-input")); + const category_subkategori = getByTestId("category-subcategory-input"); + expect(category_subkategori.children.length).toEqual(3); + expect(category_subkategori.value).toEqual( + "0664247c-d9ea-4e56-bb02-4b8463f9e14c" + ); + expect(fetch.mock.calls.length).toEqual(2); + await act(async () => { + await fireEvent.input(name_subkategori, { target: { value: "test" } }); + await fireEvent.input(category_subkategori, { + target: { value: "password" }, + }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-subcategory")); + }); + + expect(fetch.mock.calls.length).toEqual(3); +}); + +test("Test Edit subkategori error", async () => { + fetch + .once( + JSON.stringify({ + id: "cbc3dcdd-98e2-459f-b244-6f07819dda8a", + name: "avengers endgame", + category: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + category_name: "Mainan", + image: + "https://industripilar-api-staging.s3.amazonaws.com/media/uploads/subcategories/1_txtcYocQEGtOFN33ZCTDbw.png", + }) + ) + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({}), { status: 400 }); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <EditSubkategori /> + </AuthContext.Provider> + ); + const waitSubkategori = getByTestId("waiting-edit-subkategori"); + expect(waitSubkategori.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("edit-subkategori")); + const name_subkategori = getByTestId("name-subkategori-input"); + expect(name_subkategori.value).toEqual("avengers endgame"); + await waitFor(() => getByTestId("category-subcategory-input")); + const category_subkategori = getByTestId("category-subcategory-input"); + expect(category_subkategori.children.length).toEqual(3); + expect(category_subkategori.value).toEqual( + "0664247c-d9ea-4e56-bb02-4b8463f9e14c" + ); + expect(fetch.mock.calls.length).toEqual(2); + await act(async () => { + await fireEvent.input(name_subkategori, { target: { value: "test" } }); + await fireEvent.input(category_subkategori, { + target: { value: "password" }, + }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-subcategory")); + }); + const subcategory = getByTestId("edit-subkategori"); + expect(subcategory.textContent).toContain( + "Error !, Data tidak dapat disimpan" + ); + expect(fetch.mock.calls.length).toEqual(3); +}); diff --git a/src/__test__/subkategori/ListSubkategori.test.js b/src/__test__/subkategori/ListSubkategori.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c76a0228cfa82d07e1f25811a98f7ebb1669e6a2 --- /dev/null +++ b/src/__test__/subkategori/ListSubkategori.test.js @@ -0,0 +1,52 @@ +import { cleanup, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import ListSubkategori from "../../page/subkategori/ListSubkategori"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test(" Test List subkategori", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + count: 2, + next: null, + previous: null, + results: [ + { + id: "1fac049f-592c-4c15-afe6-9e05a2ce1540", + name: "Baju Pergi", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + { + id: "626aa022-50a7-4d3a-b658-79cb0f059b03", + name: "Baju Tidur", + category: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + category_name: "Baju", + image: null, + }, + { + id: "e44da96a-44e3-4e69-8346-bd1da7b8e41f", + name: "Kue", + category: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + category_name: "Makanan", + image: null, + }, + ], + }) + ); + const { getByTestId, getByText } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <ListSubkategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("tableList")); + const data = getByText("Baju Pergi"); + expect(data.textContent).toContain("Baju Pergi"); + expect(fetch.mock.calls.length).toEqual(1); +}); diff --git a/src/__test__/subkategori/TambahSubCategory.test.js b/src/__test__/subkategori/TambahSubCategory.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e8a888e1cb57fd04f2d4e9e3569a711044d5e46e --- /dev/null +++ b/src/__test__/subkategori/TambahSubCategory.test.js @@ -0,0 +1,176 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import { waitFor } from "@testing-library/dom"; +import React from "react"; +import TambahSubkategori from "../../page/subkategori/TambahSubkategori"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); +test("Test tambah subkategori renders", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahSubkategori /> + </AuthContext.Provider> + ); + const name_subkategori = getByTestId("name-subkategori-input"); + await waitFor(() => getByTestId("category-subcategory-input")); + const category_subkategori = getByTestId("category-subcategory-input"); + expect(category_subkategori.children.length).toEqual(3); + expect(fetch.mock.calls.length).toEqual(1); + await act(async () => { + await fireEvent.input(name_subkategori, { target: { value: "test" } }); + await fireEvent.input(category_subkategori, { + target: { value: "password" }, + }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-subcategory")); + }); + + expect(fetch.mock.calls.length).toEqual(2); +}); + +test("Test tambah subkategori form required", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({})); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahSubkategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("category-subcategory-input")); + const category_subkategori = getByTestId("category-subcategory-input"); + expect(category_subkategori.children.length).toEqual(3); + expect(fetch.mock.calls.length).toEqual(1); + await act(async () => { + await fireEvent.submit(getByTestId("submit-subcategory")); + }); + const formCategory = getByTestId("form-subcategory"); + expect(formCategory.textContent).toContain( + "Nama subkategori tidak boleh kosong" + ); + expect(fetch.mock.calls.length).toEqual(1); +}); + +test("Test tambah subkategori error", async () => { + fetch + .once( + JSON.stringify({ + count: 3, + next: null, + previous: null, + results: [ + { + id: "f0c08b4f-7421-4298-89e4-3d4a40ef15b4", + name: "Baju", + image: null, + }, + { + id: "0664247c-d9ea-4e56-bb02-4b8463f9e14c", + name: "Mainan", + image: null, + }, + { + id: "8c2c06e6-0ead-4b9a-8de1-37237fc6bdc9", + name: "Makanan", + image: null, + }, + ], + }) + ) + .once(JSON.stringify({}), { status: 400 }); + + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahSubkategori /> + </AuthContext.Provider> + ); + const name_subkategori = getByTestId("name-subkategori-input"); + await waitFor(() => getByTestId("category-subcategory-input")); + const category_subkategori = getByTestId("category-subcategory-input"); + expect(category_subkategori.children.length).toEqual(3); + expect(fetch.mock.calls.length).toEqual(1); + await act(async () => { + await fireEvent.input(name_subkategori, { target: { value: "test" } }); + await fireEvent.input(category_subkategori, { + target: { value: "password" }, + }); + }); + await act(async () => { + await fireEvent.submit(getByTestId("submit-subcategory")); + }); + const subcategory = getByTestId("tambah-subkategori"); + expect(subcategory.textContent).toContain( + "Error !, Data tidak dapat disimpan" + ); + expect(fetch.mock.calls.length).toEqual(2); +}); + +test("Test tambah subkategori form error", async () => { + fetch.mockResponseOnce([JSON.stringify([{}]), { status: 404 }]); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TambahSubkategori /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("form-subcategory")); + const formCategory = getByTestId("form-subcategory"); + expect(fetch.mock.calls.length).toEqual(1); + expect(formCategory.textContent).toContain( + "Error loading form !, Please relogin.." + ); +}); diff --git a/src/__test__/table.test.js b/src/__test__/table.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d69bcc599db91bab7caaa3cc8b956fb4109e45d3 --- /dev/null +++ b/src/__test__/table.test.js @@ -0,0 +1,122 @@ +import React from "react"; +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import TableComponent from "../component/TableComponent"; +import AuthContext from "../utils/contex"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("use search bar", async () => { + const data = { + url: `${process.env.REACT_APP_BASE_URL}/users/`, + pageDefault: 1, + searchDefault: "", + title: "Pengguna", + keyValuePairs: [ + ["id", "id"], + ["full_name", "Nama Lengkap"], + ["username", "Username"], + ["dummy", "dummy"], + ["dummy", "dummy"], + ], + link: "/pengguna/details/", + }; + fetch.mockResponseOnce( + JSON.stringify({ + count: 6, + next: null, + previous: null, + results: [ + { + id: "45897cc5-968c-44cf-931d-e646b095fcaf", + username: "admin-staging", + full_name: "", + phone_number: "", + address: "", + neighborhood: "", + hamlet: "", + urban_village: "", + sub_district: "", + profile_picture: null, + }, + { + id: "663392ac-1dd6-462b-9301-a19c1287cefd", + username: "dummyuser", + full_name: "Dummy User", + phone_number: "+6285212345678", + address: "Jl. Dummy No.1", + neighborhood: "000", + hamlet: "000", + urban_village: "Dummy Urban Village", + sub_district: "Dummy Sub-District", + profile_picture: null, + }, + { + id: "eafaa6d5-cc28-42bd-8fa3-e6eeecbfb2e0", + username: "mpit", + full_name: "annisaa fitri shabrina", + phone_number: "+6282190772106", + address: "jalan taman ayun vi", + neighborhood: "004", + hamlet: "016", + urban_village: "pengglingan", + sub_district: "kecamatan", + profile_picture: null, + }, + { + id: "50d94bea-c164-4722-b5c0-cc3fda9b6e9c", + username: "pidyo", + full_name: "pidypidy", + phone_number: "+628219864572", + address: "buaran", + neighborhood: "004", + hamlet: "006", + urban_village: "michael", + sub_district: "halim", + profile_picture: null, + }, + { + id: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + username: "whtestest", + full_name: "Michael Wiryadinata", + phone_number: "+628192090199", + address: "Ada deh 123", + neighborhood: "001", + hamlet: "002", + urban_village: "ada deh", + sub_district: "mau tau", + profile_picture: + "https://industripilar-api-staging.s3.amazonaws.com/media/uploads/profile/cf3456e1-e4b2-4da9-a6c6-35c3a3a47cbc6842089823804677314.jpg", + }, + { + id: "1bcf4409-4894-4f80-97af-b9f9ce47f01e", + username: "whtestest2", + full_name: "michaeleh", + phone_number: "+628192090198", + address: "zjsjsn", + neighborhood: "012", + hamlet: "003", + urban_village: "nsjsns", + sub_district: "bhjsns", + profile_picture: null, + }, + ], + }) + ); + const { getByPlaceholderText, getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "BEBAS" } }}> + <TableComponent {...data} /> + </AuthContext.Provider> + ); + const search = getByPlaceholderText("Search..."); + await act(async () => { + await fireEvent.input(search, { target: { value: "Dummy User" } }); + }); + await act(async () => { + await fireEvent.submit(getByPlaceholderText("Search...")); + }); + const table = getByTestId("table"); + expect(table.textContent).toContain("Dummy User"); +}); diff --git a/src/__test__/transaksi/DetailTransaksi.test.js b/src/__test__/transaksi/DetailTransaksi.test.js new file mode 100644 index 0000000000000000000000000000000000000000..2a04ca61bcb165aba192b1a5945b1e75011a3a47 --- /dev/null +++ b/src/__test__/transaksi/DetailTransaksi.test.js @@ -0,0 +1,195 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import DetailTransaksi from "../../page/transaksi/DetailTransaksi"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test("Test detail loaded TRANSFER", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "whtestest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "TRF", + readable_payment_method: "Transfer", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: "a", + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }) + ); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "tester" } }}> + <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} /> + </AuthContext.Provider> + ); + const wait = getByTestId("waiting-detail-transaksi"); + expect(wait.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("page-detail-transaksi")); + const data = getByTestId("page-detail-transaksi"); + expect(data.textContent).toContain("Transfer"); + await act(async () => { + await fireEvent.click(getByTestId("button-see-proof")); + }); + const close = getByTestId("button-close-proof"); + expect(close.textContent).toContain("Close"); + await act(async () => { + await fireEvent.click(close); + }); + const dropdown = getByTestId("dropdown-status"); + expect(dropdown.children.length).toEqual(6); +}); + +test("Test detail loaded COD", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "whtestest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "COD", + readable_payment_method: "Pembayaran di tempat", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: "a", + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }) + ); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "tester" } }}> + <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} /> + </AuthContext.Provider> + ); + const wait = getByTestId("waiting-detail-transaksi"); + expect(wait.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("page-detail-transaksi")); + const data = getByTestId("page-detail-transaksi"); + expect(data.textContent).toContain("Cash On Delivery"); + const dropdown = getByTestId("dropdown-status"); + expect(dropdown.children.length).toEqual(5); +}); + +test("Test detail loaded error", async () => { + fetch.mockResponseOnce(JSON.stringify({}), { status: 400 }); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "tester" } }}> + <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} /> + </AuthContext.Provider> + ); + const wait = getByTestId("waiting-detail-transaksi"); + await waitFor(() => wait); + expect(wait.textContent).toContain("Something error"); +}); + +test("Test detail loaded submit", async () => { + fetch + .once( + JSON.stringify({ + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "whtestest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "TRF", + readable_payment_method: "Transfer", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: "a", + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }) + ) + .once(JSON.stringify({}), { statusCode: 204 }); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "tester" } }}> + <DetailTransaksi idTransaksi={"0c1db3b2-48f0-4604-83ca-e8f16e8550ab"} /> + </AuthContext.Provider> + ); + const wait = getByTestId("waiting-detail-transaksi"); + expect(wait.textContent).toContain("Fetching data.."); + await waitFor(() => getByTestId("page-detail-transaksi")); + const data = getByTestId("page-detail-transaksi"); + expect(data.textContent).toContain("Transfer"); + const dropdown = getByTestId("dropdown-status"); + expect(dropdown.children.length).toEqual(6); + await act(async () => { + await fireEvent.click(getByTestId("button-submit-status")); + }); + expect(fetch.mock.calls.length).toEqual(2); +}); diff --git a/src/__test__/transaksi/ListTransaksi.test.js b/src/__test__/transaksi/ListTransaksi.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f9317b6e299407512fc5bbff8cbf7dfc33103ed1 --- /dev/null +++ b/src/__test__/transaksi/ListTransaksi.test.js @@ -0,0 +1,187 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; +import AuthContext from "../../utils/contex"; +import React from "react"; +import ListTransaksi from "../../page/transaksi/ListTransaksi"; +import { waitFor } from "@testing-library/dom"; + +beforeEach(() => { + fetch.resetMocks(); +}); +afterEach(cleanup); + +test(" Test List transaksi", async () => { + fetch.mockResponseOnce( + JSON.stringify({ + count: 17, + next: "https://industripilar-staging.herokuapp.com/transactions/?page=2", + previous: null, + results: [ + { + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "whtestest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "TRF", + readable_payment_method: "Transfer", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: null, + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }, + ], + }) + ); + const { getByTestId } = render( + <AuthContext.Provider value={{ profile: { token: "tester" } }}> + <ListTransaksi /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("tableList")); + const data = getByTestId("tableList"); + expect(data.textContent).toContain("whtestest"); + expect(data.textContent).toContain("Rp370.000"); + expect(data.textContent).toContain("18 April 2020 10:59"); + expect(fetch.mock.calls.length).toEqual(1); +}); + +test(" Test List transaksi filter", async () => { + fetch + .once( + JSON.stringify({ + count: 17, + next: + "https://industripilar-staging.herokuapp.com/transactions/?page=2", + previous: null, + results: [ + { + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "whtestest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "TRF", + readable_payment_method: "Transfer", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: null, + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }, + ], + }) + ) + .once( + JSON.stringify({ + count: 17, + next: + "https://industripilar-staging.herokuapp.com/transactions/?page=2", + previous: null, + results: [ + { + id: "0c1db3b2-48f0-4604-83ca-e8f16e8550ab", + transaction_number: "H793P5ZK", + user: "d4b98bb5-8ba4-4a41-af10-93abcf53df58", + user_username: "filterwhtest", + user_full_name: "Michael Wiryadinata halim", + user_phone_number: "+628192090199", + shipping_address: "ada deh test", + shipping_neighborhood: "002", + shipping_hamlet: "002", + shipping_urban_village: "penggilingan", + shipping_sub_district: "Dummy Sub-District", + transaction_items: [ + { + id: "37f0298c-2e8a-4ab0-a094-e9177ddc60c4", + product: "3d403cd3-e356-4c15-9a86-8843333e2778", + product_code: "5VK6TY", + product_name: "produk barang", + product_price: "50000.00", + quantity: 7, + }, + ], + transaction_item_subtotal: "350000.00", + shipping_costs: "15000.00", + payment_method: "TRF", + readable_payment_method: "Transfer", + donation: "5000.00", + transaction_status: "002", + readable_transaction_status: "Waiting for seller confirmation", + proof_of_payment: null, + user_bank_account_name: "test", + user_bank_account_number: "1232131241321", + created_at: "2020-04-18T10:59:42.074386+07:00", + updated_at: "2020-04-18T11:00:18.150633+07:00", + subtotal: "370000.00", + }, + ], + }) + ); + const { getByTestId, getByLabelText } = render( + <AuthContext.Provider value={{ profile: { token: "tester" } }}> + <ListTransaksi /> + </AuthContext.Provider> + ); + await waitFor(() => getByTestId("tableList")); + const data = getByTestId("tableList"); + expect(data.textContent).toContain("whtestest"); + expect(data.textContent).toContain("Rp370.000"); + expect(data.textContent).toContain("18 April 2020 10:59"); + expect(fetch.mock.calls.length).toEqual(1); + await act(async () => { + await fireEvent.click(getByTestId("filter-button")); + }); + await act(async () => { + await fireEvent.input( + getByLabelText("Status transaksi:", { target: { value: "002" } }) + ); + await fireEvent.click(getByTestId("submit-filter")); + }); + expect(data.textContent).toContain("filterwhtest"); + expect(fetch.mock.calls.length).toEqual(2); +}); diff --git a/src/component/LinkSidebar.jsx b/src/component/LinkSidebar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..50dc2d8e6d388b94e5e366d66c30d330d482635f --- /dev/null +++ b/src/component/LinkSidebar.jsx @@ -0,0 +1,40 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { Link, Location } from "@reach/router"; +import styled from "@emotion/styled"; +import { Center } from "./html/html"; + +const LinkSidebar = (props) => ( + <Location> + {({ location }) => { + const isActive = location.pathname === props.to; + const divColor = isActive ? "#3C8DBC" : "transparent"; + const textColor = isActive ? "white" : "black"; + return ( + <Center + css={css` + background-color: ${divColor}; + width: 100%; + text-align: center; + height: 2.5rem; + color: ${textColor}; + `} + > + <StyledLink {...props} /> + </Center> + ); + }} + </Location> +); + +export default LinkSidebar; + +const StyledLink = styled(Link)` + color: inherit; + text-decoration: none; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 17px; + letter-spacing: 0.2em; +`; diff --git a/src/component/LinkYellow.jsx b/src/component/LinkYellow.jsx new file mode 100644 index 0000000000000000000000000000000000000000..576ebd3288315f995203a2f3a5590b79c6940673 --- /dev/null +++ b/src/component/LinkYellow.jsx @@ -0,0 +1,38 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { Link, Location } from "@reach/router"; + +const LinkYellow = (props) => ( + <Location> + {({ location }) => { + const isActive = location.pathname === props.to; + const divColor = isActive ? "transparent" : "#FFC80A"; + const textColor = isActive ? "grey" : "black"; + return ( + <Link + {...props} + css={css` + display: flex; + border: 3px solid ${divColor}; + width: 100%; + text-align: center; + justify-content: center; + align-items: center; + height: 2rem; + color: ${textColor}; + box-sizing: border-box; + border-radius: 50px; + text-decoration: none; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 17px; + letter-spacing: 0.2em; + `} + /> + ); + }} + </Location> +); + +export default LinkYellow; diff --git a/src/component/Loader.jsx b/src/component/Loader.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bebc58f06145cf3c8d657b7840f181d209dd8e4d --- /dev/null +++ b/src/component/Loader.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import { usePromiseTracker } from "react-promise-tracker"; +import { Backdrop, CircularProgress } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +const useStyles = makeStyles((theme) => ({ + backdrop: { + zIndex: theme.zIndex.drawer + 1, + color: "#6200EE", + }, +})); +const Loader = () => { + const classes = useStyles(); + const { promiseInProgress } = usePromiseTracker(); + return ( + <Backdrop className={classes.backdrop} open={promiseInProgress}> + <CircularProgress color="inherit" /> + </Backdrop> + ); +}; + +export default Loader; diff --git a/src/component/Logo.jsx b/src/component/Logo.jsx new file mode 100644 index 0000000000000000000000000000000000000000..af4eb52952a034ee1ed01996be975d3169283dbe --- /dev/null +++ b/src/component/Logo.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import { css } from "@emotion/core"; + +const Logo = () => { + return ( + <div + css={css` + display: flex; + background-color: black; + color: white; + align-items: center; + justify-content: center; + width: 10rem; + height: 2.5rem; + border-radius: 3px; + font-size: 18px; + `} + > + HOME INDUSTRY + </div> + ); +}; + +export default Logo; diff --git a/src/component/Sidebar.jsx b/src/component/Sidebar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4c9841d9bcbcdedb5405508ca5fc6b59e69f425f --- /dev/null +++ b/src/component/Sidebar.jsx @@ -0,0 +1,90 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { Center } from "./html/html"; +import Logo from "./Logo"; +import LinkSidebar from "./LinkSidebar"; +import { useAuthContext } from "../utils/contex"; +import styled from "@emotion/styled"; + +const logout = (token) => { + fetch(`${process.env.REACT_APP_BASE_URL}/auth/logout/`, { + method: "POST", + headers: { + Authorization: `Token ${token}`, + }, + }); +}; + +const Sidebar = () => { + const { handleLogout, profile } = useAuthContext(); + return ( + <Center + css={css` + height: 100%; + border: 1px solid #e0e0e0; + box-sizing: border-box; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + background-color: white; + align-items: center; + justify-content: stretch; + `} + > + <div + css={css` + margin-top: 12vh; + `} + /> + <Logo /> + <div + css={css` + margin-top: 2rem; + font-style: normal; + font-weight: 600; + font-size: 24px; + line-height: 29px; + text-align: center; + letter-spacing: 0.2em; + margin-bottom: 4rem; + `} + > + ADMIN DASHBOARD + </div> + <LinkSidebar to="/produk">PRODUK</LinkSidebar> + <LinkSidebar to="/transaksi">TRANSAKSI</LinkSidebar> + <LinkSidebar to="/program">PROGRAM</LinkSidebar> + <LinkSidebar to="/product1">DONASI</LinkSidebar> + <LinkSidebar to="/pengguna">PENGGUNA</LinkSidebar> + <Center + css={css` + width: 100%; + text-align: center; + height: 2.5rem; + `} + > + <StyledA + data-testid="logout" + onClick={() => { + logout(profile.token.repeat(1)); + handleLogout(); + }} + > + LOGOUT + </StyledA> + </Center> + </Center> + ); +}; + +export default Sidebar; + +const StyledA = styled.a` + text-decoration: none; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 17px; + letter-spacing: 0.2em; + text-align: center; + color: #3c8dbc; + cursor: pointer; +`; diff --git a/src/component/Status.jsx b/src/component/Status.jsx new file mode 100644 index 0000000000000000000000000000000000000000..09f32d11cc9c8066576fff08cb0b9391d4bc59ed --- /dev/null +++ b/src/component/Status.jsx @@ -0,0 +1,42 @@ +import React from "react"; +import { css } from "@emotion/core"; +const Status = ({ status, label }) => { + let color; + switch (status) { + case "001": + color = "#EAC435"; + break; + case "002": + color = "#FB4D3D"; + break; + case "003": + color = "#FC766AFF"; + break; + case "004": + color = "#03CEA4"; + break; + case "005": + color = "#27496d"; + break; + case "006": + color = "#CA1551"; + break; + case "007": + color = "#CA1551"; + break; + default: + color = "black"; + break; + } + return ( + <div + css={css` + color: ${color}; + `} + > + {label} + </div> + ); +}; + +export default Status; diff --git a/src/component/TableComponent.jsx b/src/component/TableComponent.jsx new file mode 100644 index 0000000000000000000000000000000000000000..86f97461ba8675126e31b222bc0e02d52262fc1b --- /dev/null +++ b/src/component/TableComponent.jsx @@ -0,0 +1,302 @@ +import React, { useState } from "react"; +import useFetchList from "../utils/useFetchList"; +import { useForm } from "react-hook-form"; +import { css } from "@emotion/core"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import Toolbar from "@material-ui/core/Toolbar"; +import TableFooter from "@material-ui/core/TableFooter"; +import TablePagination from "@material-ui/core/TablePagination"; +import TableRow from "@material-ui/core/TableRow"; +import Paper from "@material-ui/core/Paper"; +import FilterListIcon from "@material-ui/icons/FilterList"; +import Collapse from "@material-ui/core/Collapse"; +import { + ButtonSearch, + ErrorDiv, + InputForm, + InputSearch, + InputSelectForm, + InputSubmitForm, + LabelInput, + RowInput, +} from "./html/html"; +import { Search } from "@material-ui/icons"; +import { Link } from "@reach/router"; +import ArrowDropDownCircleIcon from "@material-ui/icons/ArrowDropDownCircle"; +import TableHead from "@material-ui/core/TableHead"; +import Tooltip from "@material-ui/core/Tooltip"; +import IconButton from "@material-ui/core/IconButton"; +import Button from "@material-ui/core/Button"; + +const TableComponent = ({ + url, + pageDefault, + searchDefault, + title, + keyValuePairs, + link, + argument, + filter, +}) => { + const [ + results, + errorState, + count, + statePage, + stateSize, + setPage, + setStateSearch, + setPageSize, + setFilter, + ] = useFetchList([url, pageDefault, searchDefault, 5, argument, null]); + const { register, handleSubmit } = useForm(); + const { + register: registerFilter, + handleSubmit: handleSubmitFilter, + reset: resetFilter, + } = useForm(); + const [filterTab, setFilterTab] = useState(false); + return ( + <div + css={css` + display: flex; + margin: 0 3rem 3rem 3rem; + flex-direction: column; + `} + > + {errorState && <ErrorDiv>ERROR !! Please relogin.</ErrorDiv>} + <div + data-testid="table" + css={css` + margin-top: 1.5rem; + `} + > + <Paper> + <Toolbar> + <div + data-testid="page" + css={css` + font-style: normal; + font-weight: normal; + font-size: 36px; + line-height: 44px; + color: #292929; + `} + > + {title} + </div> + <div + css={css` + width: 100%; + display: flex; + justify-content: flex-end; + `} + > + <form + css={css` + display: flex; + `} + onSubmit={handleSubmit((data) => { + setStateSearch(data["search"]); + })} + > + <InputSearch + name="search" + ref={register} + placeholder="Search..." + /> + <ButtonSearch className="border-0"> + <Search /> + </ButtonSearch> + </form> + {filter !== null && filter !== undefined && ( + <Tooltip title="Filter list"> + <IconButton + data-testid="filter-button" + aria-label="filter list" + onClick={() => setFilterTab(!filterTab)} + > + <FilterListIcon /> + </IconButton> + </Tooltip> + )} + </div> + </Toolbar> + {filter !== null && filter !== undefined && ( + <Collapse in={filterTab} addEndListener={() => {}}> + <Toolbar> + <form + data-testid="filter-form" + onSubmit={handleSubmitFilter((data) => { + const filter = Object.entries(data) + .filter(([, val]) => val !== "") + .map(([key, val]) => `${key}=${val}`) + .join("&"); + setFilter(filter); + })} + css={css` + width: 100%; + display: flex; + flex-direction: column; + `} + > + {filter.map((field) => { + if (Array.isArray(field)) { + return ( + <RowInput key={field[0]}> + <LabelInput htmlFor={field[0]}> + {field[1]}: + </LabelInput> + <InputForm + id={field[0]} + type="date" + name={field[0]} + ref={registerFilter} + /> + </RowInput> + ); + } else if (typeof field === "object") { + const k = Object.keys(field); + return ( + <RowInput key={k[0]}> + <LabelInput htmlFor={k[0]}> + {field[k].label}: + </LabelInput> + <InputSelectForm + id={k[0]} + name={k[0]} + ref={registerFilter} + > + <option value="" /> + {field[k].choices.map((c) => { + const choice = Object.keys(c); + return ( + <option key={choice} value={choice}> + {c[choice]} + </option> + ); + })} + </InputSelectForm> + </RowInput> + ); + } + })} + <div + css={css` + display: flex; + width: 100%; + align-items: center; + align-content: space-between; + `} + > + <InputSubmitForm + css={css` + width: 50%; + margin-right: 2rem; + `} + data-testid="submit-filter" + type="submit" + value="Filter" + /> + <Button + variant="outlined" + color="secondary" + onClick={() => { + resetFilter(); + setFilter(null); + }} + > + Hapus Filter + </Button> + </div> + </form> + </Toolbar> + </Collapse> + )} + <TableContainer> + <Table> + <TableHead> + <TableRow> + {keyValuePairs.slice(1).map((pairs, index) => { + return ( + <TableCell + key={`${pairs[0]}${index}`} + css={css` + font-weight: bold; + `} + > + {pairs[1]} + </TableCell> + ); + })} + {link !== undefined ? ( + <TableCell + css={css` + font-weight: bold; + `} + > + Action + </TableCell> + ) : null} + </TableRow> + </TableHead> + <TableBody data-testid="tableList"> + {results.map((u, indexR) => { + return ( + <TableRow key={`row${indexR}-${u[keyValuePairs[0][0]]}`}> + {keyValuePairs.slice(1).map((pairs, indexC) => { + return ( + <TableCell key={`${pairs[0]}r${indexR}c${indexC}`}> + {pairs[2] !== undefined && pairs[2] !== null + ? pairs[0] !== "" + ? pairs[2](u[pairs[0]]) + : pairs[2](u) + : u[pairs[0]]} + </TableCell> + ); + })} + {link !== undefined ? ( + <TableCell key={`link_r${indexR}`}> + <Link to={`${link}${u.id}`}> + <ArrowDropDownCircleIcon color="action" /> + </Link> + </TableCell> + ) : null} + </TableRow> + ); + })} + </TableBody> + <TableFooter> + <TableRow> + <TablePagination + rowsPerPageOptions={[5, 7, 10]} + count={count || 0} + rowsPerPage={stateSize} + page={statePage - 1} + SelectProps={{ + inputProps: { "aria-label": "rows per page" }, + native: true, + }} + onChangePage={(event, newPage) => { + setPage(newPage + 1); + }} + onChangeRowsPerPage={(event) => { + setPageSize(parseInt(event.target.value, 10)); + setPage(1); + }} + /> + </TableRow> + </TableFooter> + </Table> + </TableContainer> + </Paper> + </div> + {/*<Paginator />*/} + </div> + ); +}; + +export default TableComponent; diff --git a/src/component/TableUtils.jsx b/src/component/TableUtils.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a12dcdbc62fdd3e879b49d0acdc8b8b3395abfda --- /dev/null +++ b/src/component/TableUtils.jsx @@ -0,0 +1,30 @@ +import React from "react"; +import Moment from "react-moment"; +import NumberFormat from "react-number-format"; +import "moment-timezone"; +import Status from "./Status"; +export const stringToDate = (date) => { + const dateFormat = new Date(date); + return ( + <Moment format={"DD MMMM YYYY HH:mm"} tz="Asia/Jakarta" date={dateFormat} /> + ); +}; + +export const stringToCurrency = (currency) => ( + <NumberFormat + value={currency} + isNumericString={true} + displayType={"text"} + thousandSeparator="." + decimalSeparator="," + decimalScale={0} + prefix={"Rp"} + /> +); + +export const transactionToColoredStatus = (transaction) => ( + <Status + status={transaction.transaction_status} + label={transaction.readable_transaction_status} + /> +); diff --git a/src/component/html/html.js b/src/component/html/html.js new file mode 100644 index 0000000000000000000000000000000000000000..446dc1cccb61ac6d939eb17c77e3b010c1586a7c --- /dev/null +++ b/src/component/html/html.js @@ -0,0 +1,118 @@ +import styled from "@emotion/styled"; + +export const Center = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +`; + +export const InputSearch = styled.input` + background: #ffffff; + border: 1px solid #e0e1e2; + box-sizing: border-box; + border-radius: 4px; + width: 15rem; + height: 2.3rem; + font-size: 1rem; + padding-left: 1rem; +`; + +export const ButtonSearch = styled.button` + border: 1px solid #e0e1e2; + border-left: none; /* Prevent double borders */ + cursor: pointer; + background-color: white; +`; + +export const ErrorDiv = styled.div` + color: red; +`; + +export const ButtonRed = styled.button` + border: 2.5px solid #cc4b4c; + background-color: white; + color: #cc4b4c; + border-radius: 50px; + width: 100%; + padding: 8px 24px; +`; + +export const ButtonSubmit = styled.button` + border: 2.5px solid #3c8dbc; + background-color: #3c8dbc; + color: white; + border-radius: 50px; + width: 100%; + padding: 8px 24px; +`; + +export const InputForm = styled.input` + flex-grow: 4; + border: 1px solid #e0e1e2; + box-sizing: border-box; + border-radius: 4px; + font-size: 1rem; + padding-left: 1rem; +`; + +export const InputTextArea = styled.textarea` + flex-grow: 4; + border: 1px solid #e0e1e2; + box-sizing: border-box; + border-radius: 4px; + font-size: 1rem; + padding-left: 1rem; +`; + +export const InputSelectForm = styled.select` + flex-grow: 4; + border: 1px solid #e0e1e2; + box-sizing: border-box; + border-radius: 4px; + font-size: 1rem; + padding-left: 1rem; +`; + +export const RowInput = styled.div` + height: 2rem; + margin: 0.5rem; + display: flex; + align-content: space-between; +`; + +export const LabelInput = styled.label` + width: 30%; + font-size: 18px; + line-height: 22px; +`; + +export const InputSubmitForm = styled.input` + width: 100%; + background: #3c8dbc; + border: 1px solid #3c8dbc; + box-sizing: border-box; + border-radius: 50px; + font-size: 14px; + line-height: 17px; + text-align: center; + letter-spacing: 0.2em; + color: white; + height: 2.3rem; +`; + +export const ButtonDeleteStyled = styled.button` + border: 4px solid #cc4b4c; + box-sizing: border-box; + border-radius: 50px; + color: #cc4b4c; + width: 100%; + height: 2rem; + margin-left: 0.5rem; + cursor: pointer; + background-color: white; + font-size: 14px; + :disabled { + opacity: 30%; + } +`; diff --git a/src/component/routes/ProtectedRoute.jsx b/src/component/routes/ProtectedRoute.jsx new file mode 100644 index 0000000000000000000000000000000000000000..595547b43ee9d29ec54f659c411e6555c66c075f --- /dev/null +++ b/src/component/routes/ProtectedRoute.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useAuthContext } from "../../utils/contex"; +import { Redirect } from "@reach/router"; +import Layout from "../../layout/Layout"; + +const ProtectedRoute = (props) => { + const { is_authenticated } = useAuthContext(); + + return is_authenticated ? ( + <Layout component={props} /> + ) : ( + <Redirect from="" to="/" noThrow /> + ); +}; + +export default ProtectedRoute; diff --git a/src/component/routes/UnauthenticatedRoute.jsx b/src/component/routes/UnauthenticatedRoute.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2fedfb4cfdf79002a95a8f5a45db013f3a2000b6 --- /dev/null +++ b/src/component/routes/UnauthenticatedRoute.jsx @@ -0,0 +1,15 @@ +import { useAuthContext } from "../../utils/contex"; +import { Redirect } from "@reach/router"; +import React from "react"; + +const UnauthenticatedRoute = ({ component: Component, ...rest }) => { + const { is_authenticated } = useAuthContext(); + + return !is_authenticated ? ( + <Component {...rest} /> + ) : ( + <Redirect from="" to="/produk" noThrow /> + ); +}; + +export default UnauthenticatedRoute; diff --git a/src/index.css b/src/index.css index ec2585e8c0bb8188184ed1e0703c4c8f2a8419b0..15320741a80ebb181131e23f6d0be4ac3989d6c0 100644 --- a/src/index.css +++ b/src/index.css @@ -6,8 +6,3 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/layout/Layout.jsx b/src/layout/Layout.jsx new file mode 100644 index 0000000000000000000000000000000000000000..85851d57b240cfdfd9d83f3866a907bc8583ea63 --- /dev/null +++ b/src/layout/Layout.jsx @@ -0,0 +1,76 @@ +import React from "react"; +import { css } from "@emotion/core"; +import Sidebar from "../component/Sidebar"; +import { Center } from "../component/html/html"; + +const Layout = (props) => { + const { component: Component, ...rest } = props["component"]; + return ( + <div + css={css` + display: flex; + height: 100vh; + `} + > + <div + css={css` + width: 60vh; + `} + > + <Sidebar /> + </div> + <Center + css={css` + width: 100%; + height: 100%; + `} + > + <div + css={css` + background-color: white; + width: 90%; + height: 85%; + border: 1px solid #e0e0e0; + box-sizing: border-box; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + overflow: scroll; + `} + > + <Component {...rest} /> + </div> + </Center> + <div + css={css` + background-color: black; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 5px; + `} + /> + <div + css={css` + background-color: #75a9c7; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 18px; + `} + /> + <div + css={css` + background-color: #3c8dbc; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 10px; + `} + /> + </div> + ); +}; + +export default Layout; diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 6b60c1042f58d9fabb75485aa3624dddcf633b5c..0000000000000000000000000000000000000000 --- a/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> - <g fill="#61DAFB"> - <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> - <circle cx="420.9" cy="296.5" r="45.7"/> - <path d="M520.5 78.1z"/> - </g> -</svg> diff --git a/src/page/kategori/DetailKategori.jsx b/src/page/kategori/DetailKategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..19edb2411b7ee663fdb24281f46ccfc4e1a7fef7 --- /dev/null +++ b/src/page/kategori/DetailKategori.jsx @@ -0,0 +1,140 @@ +import React, { useState } from "react"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import { css } from "@emotion/core"; +import { ButtonDeleteStyled, ErrorDiv } from "../../component/html/html"; +import TableComponent from "../../component/TableComponent"; +import LinkYellow from "../../component/LinkYellow"; +import { navigate } from "@reach/router"; +import { ArrowBack } from "@material-ui/icons"; +import useDelete from "../../utils/useDelete"; +import { + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Button, +} from "@material-ui/core"; + +const DetailKategori = ({ idKategori }) => { + const url = `${process.env.REACT_APP_BASE_URL}/categories/${idKategori}/`; + const [category, error] = useFetchSingleData(url); + const [deleteProduct, errorDelete] = useDelete(url); + const [openModal, setOpenModal] = useState(false); + const data = { + url: `${process.env.REACT_APP_BASE_URL}/products/`, + pageDefault: 1, + searchDefault: "", + title: "", + keyValuePairs: [ + ["id", "id"], + ["name", "Nama"], + ["price", "Harga"], + ["stock", "Stok"], + ["subcategory_name", "Subcategory"], + ], + argument: `subcategory__category=${idKategori}`, + }; + const handleClose = () => setOpenModal(false); + return ( + <div + data-testid="page-detail-kategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + <Dialog open={openModal} onClose={handleClose}> + <DialogTitle>{`Hapus kategori ${category.name}`}</DialogTitle> + <DialogContent> + <DialogContentText> + Apakah yakin untuk menghapus kategori {category.name}? + </DialogContentText> + <DialogActions> + <Button onClick={handleClose} color="primary"> + TIDAK + </Button> + <Button + data-testid="button-delete-category" + color="secondary" + onClick={() => { + deleteProduct(); + handleClose(); + }} + > + HAPUS + </Button> + </DialogActions> + </DialogContent> + </Dialog> + {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>} + {errorDelete && ( + <ErrorDiv> + Tidak dapat menghapus kategori, mohon periksa apakah ada produk + didalam kategori ini. + </ErrorDiv> + )} + <div + css={css` + display: flex; + flex-direction: column; + `} + > + <button + css={css` + align-self: start; + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate("/kategori")} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + font-size: 2rem; + display: flex; + flex-direction: row; + align-items: baseline; + margin-bottom: 1rem; + `} + > + <div + css={css` + flex-grow: 2; + `} + > + Kategori {category.name} + </div> + <div + css={css` + flex-grow: 1; + `} + > + <LinkYellow to="ubah">EDIT</LinkYellow> + </div> + <div + css={css` + flex-grow: 1; + `} + > + <ButtonDeleteStyled + data-testid="button-delete-category-modal" + onClick={() => setOpenModal(true)} + > + HAPUS + </ButtonDeleteStyled> + </div> + </div> + </div> + <TableComponent {...data} /> + </div> + ); +}; + +export default DetailKategori; diff --git a/src/page/kategori/EditKategori.jsx b/src/page/kategori/EditKategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bd5a22627bbfed9853adeafff1edd749df63dbc3 --- /dev/null +++ b/src/page/kategori/EditKategori.jsx @@ -0,0 +1,93 @@ +import React from "react"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormKategori from "./FormKategori"; +import { ArrowBack, ErrorOutline } from "@material-ui/icons"; +import { navigate } from "@reach/router"; +import useSendData from "../../utils/useSendData"; + +const EditKategori = ({ idKategori }) => { + const url = `${process.env.REACT_APP_BASE_URL}/categories/${idKategori}/`; + const [initialData, errorState] = useFetchSingleData(url); + const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 }); + const onSubmit = (data) => { + const formData = new FormData(); + formData.append("name", data["name"]); + if (data["image"].length !== 0) formData.append("image", data["image"][0]); + send(formData); + }; + if (errorState || Object.keys(initialData).length === 0) + return ( + <div + data-testid="waiting-edit-kategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + font-size: 25px; + `} + > + Fetching data.. + </div> + ); + return ( + <div + data-testid="edit-kategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>} + <div + css={css` + display: flex; + flex-direction: row; + `} + > + <button + css={css` + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate(-1)} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + font-size: 36px; + `} + > + Edit {initialData.name} + </div> + </div> + <div + css={css` + margin-top: 2.5rem; + display: flex; + `} + > + <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} /> + <div + css={css` + font-weight: 600; + font-size: 24px; + line-height: 29px; + `} + > + Informasi Kategori + </div> + </div> + <FormKategori {...{ onSubmit, initialData }} /> + </div> + ); +}; + +export default EditKategori; diff --git a/src/page/kategori/FormKategori.jsx b/src/page/kategori/FormKategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..58b15eef49f0c91355516a22339909ae74add6c9 --- /dev/null +++ b/src/page/kategori/FormKategori.jsx @@ -0,0 +1,55 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { + ErrorDiv, + RowInput, + InputForm, + LabelInput, + InputSubmitForm, +} from "../../component/html/html"; +import { css } from "@emotion/core"; + +const FormKategori = ({ onSubmit, initialData = null }) => { + const { register, handleSubmit, errors } = useForm({ + defaultValues: initialData !== null ? { name: initialData["name"] } : {}, + }); + return ( + <form + data-testid="form-category" + onSubmit={handleSubmit(onSubmit)} + css={css` + display: flex; + flex-direction: column; + `} + > + <RowInput> + <LabelInput htmlFor="name">Nama kategori </LabelInput> + <InputForm + data-testid="name-kategori-input" + name="name" + ref={register({ required: true })} + /> + {errors.name && <ErrorDiv>Nama kategori tidak boleh kosong</ErrorDiv>} + </RowInput> + {initialData !== null && initialData["image"] != null ? ( + <img + css={css` + height: 10rem; + object-fit: contain; + `} + alt={initialData["name"]} + src={initialData["image"]} + /> + ) : null} + <RowInput> + <LabelInput htmlFor="gambar">Foto kategori </LabelInput> + <InputForm type="file" name="image" ref={register} /> + </RowInput> + <RowInput> + <InputSubmitForm type="submit" data-testid="submit-category" /> + </RowInput> + </form> + ); +}; + +export default FormKategori; diff --git a/src/page/kategori/ListKategori.jsx b/src/page/kategori/ListKategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7482c1b21b98630f613b3175f7dd104edd3ff0d7 --- /dev/null +++ b/src/page/kategori/ListKategori.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import TableComponent from "../../component/TableComponent"; +import { css } from "@emotion/core"; +import LinkYellow from "../../component/LinkYellow"; + +const ListKategori = () => { + const data = { + url: `${process.env.REACT_APP_BASE_URL}/categories/`, + pageDefault: 1, + searchDefault: "", + title: "", + keyValuePairs: [ + ["id", "id"], + ["name", "Nama"], + ], + link: "", + }; + return ( + <div + css={css` + display: flex; + flex-direction: column; + margin: 2rem 3rem 3rem 3rem; + `} + > + <div + css={css` + font-size: 35px; + `} + > + KELOLA KATEGORI + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 2rem; + margin-top: 1rem; + `} + > + <LinkYellow to="/kategori/tambah">TAMBAH</LinkYellow> + <LinkYellow to="/kategori">LIHAT</LinkYellow> + </div> + <TableComponent {...data} /> + </div> + ); +}; + +export default ListKategori; diff --git a/src/page/kategori/TambahKategori.jsx b/src/page/kategori/TambahKategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..64c3b8611a00745daf1a649fa34eeaeee879f2c6 --- /dev/null +++ b/src/page/kategori/TambahKategori.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormKategori from "./FormKategori"; +import LinkYellow from "../../component/LinkYellow"; +import useSendData from "../../utils/useSendData"; + +const TambahKategori = () => { + const url = `${process.env.REACT_APP_BASE_URL}/categories/`; + const [send, error] = useSendData({ + url, + method: "POST", + redirect: "/kategori", + }); + const onSubmit = (data) => { + const formData = new FormData(); + formData.append("name", data["name"]); + if (data["image"].length !== 0) formData.append("image", data["image"][0]); + send(formData); + }; + return ( + <div + data-testid="tambah-kategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + <div> + {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>} + </div> + + <div + css={css` + font-size: 35px; + `} + > + TAMBAH KATEGORI + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 2rem; + margin-top: 1rem; + `} + > + <LinkYellow to="/kategori/tambah">TAMBAH</LinkYellow> + <LinkYellow to="/kategori">LIHAT</LinkYellow> + </div> + <FormKategori {...{ onSubmit }} /> + </div> + ); +}; + +export default TambahKategori; diff --git a/src/page/login/Login.jsx b/src/page/login/Login.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8f68fa7e1086a0e6340b5afbdf5a5bdd8a44fe14 --- /dev/null +++ b/src/page/login/Login.jsx @@ -0,0 +1,147 @@ +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useAuthContext } from "../../utils/contex"; +import { css } from "@emotion/core"; +import Logo from "../../component/Logo"; +import styled from "@emotion/styled"; +import { Center, ErrorDiv } from "../../component/html/html"; +import { trackPromise } from "react-promise-tracker"; +const Login = () => { + const { register, handleSubmit, errors } = useForm(); + const { handleLogin } = useAuthContext(); + const [errorState, setErrorState] = useState(false); + const onSubmit = (data) => { + trackPromise( + fetch(`${process.env.REACT_APP_BASE_URL}/auth/cred-login/`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...data }), + }) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + throw new Error("Error"); + } + }) + .then((result) => { + handleLogin({ ...result }); + }) + .catch(() => { + setErrorState(true); + }) + ); + }; + return ( + <Center + css={css` + height: 100vh; + `} + > + <Logo /> + <Center + css={css` + margin-top: 1rem; + height: 75vh; + background-color: white; + width: 60vh; + border: 0.5px solid #eeeeee; + box-sizing: border-box; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 10px; + `} + > + <div + css={css` + display: flex; + font-weight: 600; + font-size: 36px; + line-height: 44px; + text-align: center; + `} + > + Sign in + </div> + <form + css={css` + display: flex; + flex-direction: column; + width: 80%; + margin-top: 3rem; + `} + data-testid="login" + onSubmit={handleSubmit(onSubmit)} + > + {/* register your input into the hook by invoking the "register" function */} + + {/* include validation with required or other standard HTML validation rules */} + <FieldLogin + name="username" + placeholder="Username" + data-testid="name-input" + ref={register({ required: true })} + /> + {/* errors will return when field validation fails */} + {errors.username && ( + <span + css={css` + color: darkred; + `} + data-testid="name-required" + > + Username field is required + </span> + )} + <FieldLogin + css={css` + margin-top: 2rem; + `} + name="password" + placeholder="password" + type="password" + data-testid="password-input" + ref={register({ required: true })} + /> + {errors.password && ( + <span + css={css` + color: darkred; + `} + data-testid="password-required" + > + Password field is required + </span> + )} + {errorState && <ErrorDiv>Password salah !</ErrorDiv>} + <LoginButton + css={css` + margin-top: 2rem; + `} + type="submit" + value="LOGIN" + /> + </form> + </Center> + </Center> + ); +}; + +export default Login; + +const FieldLogin = styled.input` + background: #f6f6f6; + border: 1px solid #dedede; + box-sizing: border-box; + border-radius: 4px; + height: 2.5rem; +`; + +const LoginButton = styled.input` + background: #4e7891; + border-radius: 50px; + color: white; + height: 2.5rem; +`; diff --git a/src/page/pengguna/DetailPengguna.jsx b/src/page/pengguna/DetailPengguna.jsx new file mode 100644 index 0000000000000000000000000000000000000000..69445b4d17fcfe59daaa7977714f30fc6cc4cb7e --- /dev/null +++ b/src/page/pengguna/DetailPengguna.jsx @@ -0,0 +1,231 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { ArrowBack } from "@material-ui/icons"; +import { navigate } from "@reach/router"; +import PersonIcon from "@material-ui/icons/Person"; +import PhoneIcon from "@material-ui/icons/Phone"; +import LocationOnIcon from "@material-ui/icons/LocationOn"; +import ScheduleIcon from "@material-ui/icons/Schedule"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import { ErrorDiv } from "../../component/html/html"; +import { stringToCurrency, stringToDate } from "../../component/TableUtils"; +import TableComponent from "../../component/TableComponent"; + +const DetailPengguna = ({ userId }) => { + const url = `${process.env.REACT_APP_BASE_URL}/users/${userId}/`; + const [user, error] = useFetchSingleData(url); + const data = { + url: `${process.env.REACT_APP_BASE_URL}/transactions/`, + pageDefault: 1, + argument: `user=${userId}`, + title: "Riwayat Transaksi", + keyValuePairs: [ + ["id", "id"], + ["transaction_number", "ID Transaksi"], + ["created_at", "Tanggal Pembuatan", stringToDate], + ["updated_at", "Tanggal Update", stringToDate], + ["readable_transaction_status", "Status"], + ["subtotal", "Total", stringToCurrency], + ], + link: "/transaksi/", + filter: [ + ["updated_at_before", "Updated At Before"], + ["updated_at_after", "Updated At After"], + { + transaction_status: { + label: "Status transaksi", + choices: [ + { "001": "Waiting for proof of payment" }, + { "002": "Waiting for seller confirmation" }, + { "003": "In process" }, + { "004": "Being shipped" }, + { "005": "Completed" }, + { "006": "Canceled" }, + ], + }, + }, + ], + }; + return ( + <div + data-testid="page-profile" + css={css` + width: 100%; + height: 100%; + `} + > + {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>} + <div + css={css` + margin: 2rem 3rem 3rem 3rem; + display: flex; + flex-direction: column; + `} + > + <div + css={css` + font-style: normal; + font-weight: 300; + font-size: 2.5rem; + line-height: 3.4rem; + `} + > + <button + css={css` + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate(-1)} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + display: inline; + margin-left: 2rem; + `} + > + {user.username} + </div> + </div> + <div + className="profile" + css={css` + font-style: normal; + font-weight: 300; + font-size: 36px; + line-height: 44px; + margin-top: 1rem; + `} + > + Profil + </div> + <div + css={css` + margin-top: 1rem; + margin-bottom: 1rem; + `} + > + <div data-testid="profile"> + <div + css={css` + margin-top: 1rem; + `} + > + <PersonIcon style={{ fontSize: 20, color: "FFC80A" }} /> + <div + css={css` + display: inline; + margin-left: 2rem; + font-style: normal; + font-weight: normal; + font-size: 18px; + line-height: 22px; + `} + > + {user.username} + </div> + </div> + <div + css={css` + margin-top: 1rem; + `} + > + <PhoneIcon style={{ fontSize: 20, color: "FFC80A" }} /> + <div + css={css` + display: inline; + margin-left: 2rem; + font-style: normal; + font-weight: normal; + font-size: 18px; + line-height: 22px; + `} + > + {user.phone_number} + </div> + </div> + <div + css={css` + margin-top: 1rem; + `} + > + <LocationOnIcon style={{ fontSize: 20, color: "FFC80A" }} /> + <div + css={css` + display: inline; + margin-left: 2rem; + font-style: normal; + font-weight: normal; + font-size: 18px; + line-height: 22px; + `} + > + {user.address}, RT {user.neighborhood}, RW {user.hamlet}, + Kelurahan {user.urban_village}, Kecamatan {user.sub_district} + </div> + </div> + </div> + </div> + <div + css={css` + font-style: normal; + font-weight: 300; + font-size: 30px; + line-height: 44px; + `} + > + Riwayat + </div> + <div + css={css` + font-style: normal; + font-weight: normal; + font-size: 15px; + line-height: 25px; + margin-top: 1rem; + `} + > + <div + css={css` + display: flex; + flex-direction: column; + `} + > + <TableComponent {...data} /> + </div> + </div> + <div + css={css` + font-style: normal; + font-weight: normal; + font-size: 15px; + line-height: 15px; + margin-top: 1rem; + `} + > + <div + css={css` + display: flex; + `} + > + <ScheduleIcon style={{ fontSize: 25, color: "FFC80A" }} /> + <div + css={css` + margin-left: 2rem; + `} + > + Riwayat Donasi + </div> + </div> + </div> + </div> + </div> + ); +}; + +export default DetailPengguna; diff --git a/src/page/pengguna/ListPengguna.jsx b/src/page/pengguna/ListPengguna.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3ec37588d8bc70c0033c483b3fdba0f31d35b8c4 --- /dev/null +++ b/src/page/pengguna/ListPengguna.jsx @@ -0,0 +1,31 @@ +import React from "react"; +import { css } from "@emotion/core"; +import TableComponent from "../../component/TableComponent"; + +const ListPengguna = () => { + const data = { + url: `${process.env.REACT_APP_BASE_URL}/users/`, + pageDefault: 1, + searchDefault: "", + title: "Pengguna", + keyValuePairs: [ + ["id", "id"], + ["full_name", "Nama Lengkap"], + ["username", "Username"], + ["dummy", "dummy"], + ["dummy", "dummy"], + ], + link: "", + }; + return ( + <div + css={css` + margin-top: 2rem; + `} + > + <TableComponent {...data} /> + </div> + ); +}; + +export default ListPengguna; diff --git a/src/page/produk/DetailProduk.jsx b/src/page/produk/DetailProduk.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e732aed56195bfb2da9bd971f4c80cb0610ef8d0 --- /dev/null +++ b/src/page/produk/DetailProduk.jsx @@ -0,0 +1,309 @@ +import React, { useState } from "react"; +import { css } from "@emotion/core"; +import { ButtonDeleteStyled } from "../../component/html/html"; +import LinkYellow from "../../component/LinkYellow"; +import { ErrorDiv } from "../../component/html/html"; +import { + Search, + ArrowBack, + Grade, + Category, + AttachMoney, + LocalShipping, +} from "@material-ui/icons"; +import { Link, navigate } from "@reach/router"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import useDelete from "../../utils/useDelete"; +import { + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Button, +} from "@material-ui/core"; +import { stringToCurrency } from "../../component/TableUtils"; + +const DetailProduk = ({ productId }) => { + const url = `${process.env.REACT_APP_BASE_URL}/products/${productId}/`; + const [product, error] = useFetchSingleData(url); + const [openModal, setOpenModal] = useState(false); + const [deleteProduct, errorDelete] = useDelete(url); + const handleClose = () => setOpenModal(false); + return ( + <div + data-testid="page" + css={css` + width: 100%; + height: 100%; + `} + > + <Dialog open={openModal} onClose={handleClose}> + <DialogTitle>{`Hapus produk ${product.name}`}</DialogTitle> + <DialogContent> + <DialogContentText> + Apakah yakin untuk menghapus produk {product.name}? + </DialogContentText> + <DialogActions> + <Button onClick={handleClose} color="primary"> + TIDAK + </Button> + <Button + data-testid="button-delete-product" + color="secondary" + onClick={() => { + deleteProduct(); + handleClose(); + }} + > + HAPUS + </Button> + </DialogActions> + </DialogContent> + </Dialog> + {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>} + {errorDelete && ( + <ErrorDiv> + Tidak dapat menghapus produk, mohon periksa apakah ada produk ini. + </ErrorDiv> + )} + <div + css={css` + margin: 2rem 3rem 3rem 3rem; + display: flex; + flex-direction: column; + `} + > + <div + css={css` + font-style: normal; + font-weight: 300; + font-size: 2.5rem; + line-height: 3.4rem; + `} + > + <button + css={css` + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate(-1)} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + display: inline; + margin-left: 2rem; + `} + > + KELOLA PRODUK + </div> + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 1.8rem; + margin-top: 1rem; + `} + > + <LinkYellow to="/produk/tambah">TAMBAH</LinkYellow> + <LinkYellow className="ml-2" to="/produk"> + LIHAT + </LinkYellow> + </div> + <div + css={css` + margin-top: 3%; + `} + > + <span> + <span + className="icon" + css={css` + vertical-align: middle; + display: inline-block; + `} + > + <Search style={{ color: "#FFC80A" }} /> + </span> + <span + className="text" + css={css` + margin-top: 0.5rem; + margin-left: 1%; + font-weight: bold; + `} + > + Lihat Detail Produk + </span> + </span> + </div> + <div className="container-fluid px-0"> + <div className="row mt-2"> + <div className="col-sm-4 mt-2"> + <img + alt={product.name} + className="img-fluid" + src={product.image} + /> + </div> + <div data-testid="produk-detail" className="col-sm-8"> + <div> + <p> + <span + css={css` + font-size: 150%; + `} + > + {product.name} + </span> + </p> + <span> + <span + className="icon" + css={css` + vertical-align: middle; + display: inline-block; + `} + > + <Grade style={{ color: "#FFC80A" }} /> + </span> + <span + className="text" + css={css` + margin-top: 1rem; + margin-left: 1%; + `} + > + <Link to={`/kategori/${product.category}`}> + {product.category_name} + </Link> + </span> + </span> + <p> + <span> + <span + className="icon" + css={css` + vertical-align: middle; + display: inline-block; + `} + > + <Category style={{ color: "#FFC80A" }} /> + </span> + <span + className="text" + css={css` + margin-top: 1rem; + margin-left: 1%; + `} + > + <Link to={`/subkategori/${product.subcategory}`}> + {product.subcategory_name} + </Link> + </span> + </span> + </p> + <p> + <span> + <span + className="icon" + css={css` + vertical-align: middle; + display: inline-block; + `} + > + <AttachMoney style={{ color: "#FFC80A" }} /> + </span> + <span + className="text" + css={css` + margin-top: 1rem; + margin-left: 1%; + `} + > + {stringToCurrency(product.price)} + </span> + </span> + </p> + <p> + <span> + <span + className="icon" + css={css` + vertical-align: middle; + display: inline-block; + `} + > + <LocalShipping style={{ color: "#FFC80A" }} /> + </span> + <span + className="text" + css={css` + margin-top: 1rem; + margin-left: 1%; + `} + > + {product.pre_order ? "Preorder" : product.stock} + </span> + </span> + </p> + <span + css={css` + font-weight: bold; + `} + > + Deskripsi + </span> + <p> + <span>{product.description}</span> + </p> + </div> + <div className="container-fluid row"> + <div className="col-6"> + <div + css={css` + flex-grow: 1; + `} + > + <LinkYellow + css={css` + border: 3px solid #3c8dbc; + `} + to="ubah" + > + UBAH + </LinkYellow> + </div> + </div> + <div className="col-6"> + <div + css={css` + flex-grow: 1; + `} + > + <ButtonDeleteStyled + data-testid="button-delete-product-modal" + onClick={() => setOpenModal(true)} + > + HAPUS + </ButtonDeleteStyled> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + ); +}; + +export default DetailProduk; diff --git a/src/page/produk/EditProduk.jsx b/src/page/produk/EditProduk.jsx new file mode 100644 index 0000000000000000000000000000000000000000..365c310a30735ff51ac626d3663168eb5de3f646 --- /dev/null +++ b/src/page/produk/EditProduk.jsx @@ -0,0 +1,91 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormProduk from "./FormProduk"; +import { navigate } from "@reach/router"; +import { ArrowBack, ErrorOutline } from "@material-ui/icons"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import useSendData from "../../utils/useSendData"; + +const EditProduk = ({ productId }) => { + const url = `${process.env.REACT_APP_BASE_URL}/products/${productId}/`; + const [initialData, errorState] = useFetchSingleData(url); + const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 }); + const onSubmit = (data) => { + send(data); + }; + if (errorState || Object.keys(initialData).length === 0) + return ( + <div + data-testid="waiting-edit-produk" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + font-size: 25px; + `} + > + Fetching data.. + </div> + ); + return ( + <div + data-testid="edit-produk" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>} + <div + css={css` + display: flex; + flex-direction: row; + `} + > + <button + data-testid="back" + css={css` + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate(-1)} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + font-size: 36px; + `} + > + Edit {initialData.name} + </div> + </div> + <div + css={css` + margin-top: 2.5rem; + display: flex; + `} + > + <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} /> + <div + css={css` + font-weight: 600; + font-size: 24px; + line-height: 29px; + `} + > + Informasi Produk + </div> + </div> + <FormProduk {...{ onSubmit, initialData }} /> + </div> + ); +}; + +export default EditProduk; diff --git a/src/page/produk/FormProduk.jsx b/src/page/produk/FormProduk.jsx new file mode 100644 index 0000000000000000000000000000000000000000..267adc119f4ef8a808916497148685c9d8102565 --- /dev/null +++ b/src/page/produk/FormProduk.jsx @@ -0,0 +1,210 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import useFetchList from "../../utils/useFetchList"; +import { + ErrorDiv, + RowInput, + InputForm, + LabelInput, + InputSelectForm, + InputSubmitForm, + InputTextArea, +} from "../../component/html/html"; +import { css } from "@emotion/core"; + +const FormProduk = ({ onSubmit, initialData = null }) => { + const { register, handleSubmit, errors, watch } = useForm({ + defaultValues: + initialData !== null + ? { + name: initialData["name"], + category: initialData["category"], + subcategory: initialData["subcategory"], + description: initialData["description"], + price: initialData["price"], + stock: initialData["stock"], + pre_order: initialData["pre_order"].toString(), + } + : {}, + }); + const url = `${process.env.REACT_APP_BASE_URL}/categories/`; + const urlSubcategory = `${process.env.REACT_APP_BASE_URL}/subcategories/`; + const [results, errorState, , , , , ,] = useFetchList([ + url, + null, + null, + null, + null, + ]); + const watchCategory = watch("category", ""); + const watchPreorder = watch("pre_order"); + const [resultsSubcategory, errorStateSubcategory, , , , , ,] = useFetchList([ + urlSubcategory, + null, + null, + null, + `category=${watchCategory}`, + null, + ]); + const filterSubmit = (data) => { + const formData = new FormData(); + if (watchPreorder === "false") { + data.pre_order = false; + formData.append("stock", data["stock"]); + } else { + data.pre_order = true; + } + formData.append("name", data["name"]); + formData.append("subcategory", data["subcategory"]); + formData.append("description", data["description"]); + formData.append("price", data["price"]); + formData.append("pre_order", data["pre_order"]); + if (data["image"].length !== 0) formData.append("image", data["image"][0]); + onSubmit(formData); + }; + return ( + <form + data-testid="form-produk" + onSubmit={handleSubmit(filterSubmit)} + css={css` + display: flex; + flex-direction: column; + `} + > + <div> + {errorState || + (errorStateSubcategory && ( + <ErrorDiv>Error loading form !, Please relogin..</ErrorDiv> + ))} + </div> + <RowInput> + <LabelInput htmlFor="name">Nama Produk </LabelInput> + <InputForm + data-testid="name-produk-input" + name="name" + ref={register({ required: true })} + /> + {errors.name && <ErrorDiv>Nama Produk tidak boleh kosong</ErrorDiv>} + </RowInput> + {results === undefined || Object.keys(results).length === 0 ? null : ( + <RowInput> + <LabelInput htmlFor="name">Kategori: </LabelInput> + <InputSelectForm + data-testid="category-produk-input" + name="category" + ref={register({ required: true })} + > + {results.map((item) => ( + <option key={item.id} value={item.id}> + {item.name} + </option> + ))} + </InputSelectForm> + </RowInput> + )} + {resultsSubcategory === undefined || + results === undefined || + Object.keys(resultsSubcategory).length === 0 || + Object.keys(results).length === 0 ? null : ( + <RowInput> + <LabelInput htmlFor="name">Subkategori </LabelInput> + <InputSelectForm + data-testid="subcategory-produk-input" + name="subcategory" + ref={register({ required: true })} + > + {resultsSubcategory.map((item) => ( + <option key={item.id} value={item.id}> + {item.name} + </option> + ))} + </InputSelectForm> + </RowInput> + )} + <RowInput> + <LabelInput htmlFor="deskripsi">Deskripsi Produk </LabelInput> + <InputTextArea + data-testid="desc-produk-input" + name="description" + ref={register({ required: true })} + /> + {errors.description && ( + <ErrorDiv>Deskripsi Produk tidak boleh kosong</ErrorDiv> + )} + </RowInput> + <RowInput> + <LabelInput htmlFor="is-preorder">Tipe Barang:</LabelInput> + <div + css={css` + display: flex; + flex-grow: 4; + align-items: baseline; + align-content: flex-start; + `} + > + <input + data-testid="is-preorder-produk-input" + type="radio" + name="pre_order" + value="false" + id="no-preorder" + ref={register({ required: true })} + /> + <label htmlFor="no-preorder">Biasa</label> + </div> + <div + css={css` + display: flex; + flex-grow: 4; + align-items: baseline; + align-content: flex-start; + `} + > + <input + data-testid="is-preorder-produk-input-2" + type="radio" + name="pre_order" + value="true" + id="preorder" + ref={register({ required: true })} + /> + <label htmlFor="preorder">Preorder</label> + </div> + {errors.pre_order && ( + <ErrorDiv>Tipe Produk tidak boleh kosong</ErrorDiv> + )} + </RowInput> + <RowInput> + <LabelInput htmlFor="harga">Harga/kuantitas </LabelInput> + <InputForm + data-testid="price-produk-input" + type="number" + name="price" + ref={register({ required: true })} + /> + {errors.price && <ErrorDiv>Harga Produk tidak boleh kosong</ErrorDiv>} + </RowInput> + <RowInput> + <LabelInput htmlFor="stock">Stok</LabelInput> + <InputForm + data-testid="stock-produk-input" + type="number" + name="stock" + disabled={watchPreorder === "true"} + ref={register({ required: watchPreorder === "false" })} + /> + {errors.stock && <ErrorDiv>Stok Produk tidak boleh kosong</ErrorDiv>} + </RowInput> + + <RowInput> + <LabelInput htmlFor="gambar">Foto Produk </LabelInput> + <InputForm type="file" name="image" ref={register} /> + </RowInput> + <RowInput> + <InputSubmitForm type="submit" data-testid="submit-produk" /> + </RowInput> + </form> + ); +}; + +export default FormProduk; diff --git a/src/page/produk/ListProduk.jsx b/src/page/produk/ListProduk.jsx new file mode 100644 index 0000000000000000000000000000000000000000..021f6bd253f8e45f178062ce8e82494f9e12d30c --- /dev/null +++ b/src/page/produk/ListProduk.jsx @@ -0,0 +1,73 @@ +import React from "react"; +import TableComponent from "../../component/TableComponent"; +import { css } from "@emotion/core"; +import LinkYellow from "../../component/LinkYellow"; +import { stringToCurrency } from "../../component/TableUtils"; + +const ListProduk = () => { + const data = { + url: `${process.env.REACT_APP_BASE_URL}/products/`, + pageDefault: 1, + searchDefault: "", + title: "", + keyValuePairs: [ + ["id", "id"], + ["name", "Nama Produk"], + ["price", "Harga", stringToCurrency], + ["stock", "Stok"], + ], + link: "", + }; + return ( + <div + css={css` + display: flex; + flex-direction: column; + margin: 2rem 3rem 3rem 3rem; + `} + > + <div + css={css` + font-size: 35px; + `} + > + KELOLA PRODUK + </div> + <div + css={css` + width: 100%; + justify-content: space-between; + display: flex; + flex-direction: row; + margin-bottom: 1.8rem; + margin-top: 1rem; + `} + > + <div + css={css` + display: flex; + width: 25%; + `} + > + <LinkYellow to="tambah">TAMBAH</LinkYellow> + <LinkYellow to="/produk">LIHAT</LinkYellow> + </div> + + <div + css={css` + display: flex; + width: 35%; + `} + > + <LinkYellow to="/subkategori">SUBKATEGORI</LinkYellow> + <LinkYellow className="ml-2" to="/kategori"> + KATEGORI + </LinkYellow> + </div> + </div> + <TableComponent {...data} /> + </div> + ); +}; + +export default ListProduk; diff --git a/src/page/produk/TambahProduk.jsx b/src/page/produk/TambahProduk.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9ca23f57dad1449533b50612d00936cb09a4c66d --- /dev/null +++ b/src/page/produk/TambahProduk.jsx @@ -0,0 +1,55 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormProduk from "./FormProduk"; +import LinkYellow from "../../component/LinkYellow"; +import useSendData from "../../utils/useSendData"; + +const TambahProduk = () => { + const url = `${process.env.REACT_APP_BASE_URL}/products/`; + const [send, error] = useSendData({ + url, + method: "POST", + redirect: "/produk", + }); + const onSubmit = (data) => { + send(data); + }; + return ( + <div + data-testid="tambah-produk" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + <div> + {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>} + </div> + + <div + css={css` + font-size: 35px; + `} + > + TAMBAH PRODUK + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 2rem; + margin-top: 1rem; + `} + > + <LinkYellow to="/produk/tambah">TAMBAH</LinkYellow> + <LinkYellow to="/produk">LIHAT</LinkYellow> + </div> + <FormProduk {...{ onSubmit }} /> + </div> + ); +}; + +export default TambahProduk; diff --git a/src/page/program/DetailProgram.jsx b/src/page/program/DetailProgram.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e5da33269c1c9d4650f8d13000410e9b643c7ef3 --- /dev/null +++ b/src/page/program/DetailProgram.jsx @@ -0,0 +1,211 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { ButtonDeleteStyled, ErrorDiv } from "../../component/html/html"; +import { ArrowBack } from "@material-ui/icons"; +import { Link } from "@reach/router"; +import PersonIcon from "@material-ui/icons/Person"; +import LocationOnIcon from "@material-ui/icons/LocationOn"; +import EventIcon from "@material-ui/icons/Event"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import LinkYellow from "../../component/LinkYellow"; +import useDelete from "../../utils/useDelete"; + +const DetailPengguna = ({ idProgram }) => { + const url = `${process.env.REACT_APP_BASE_URL}/programs/${idProgram}/`; + const [program, error] = useFetchSingleData(url); + const [deleteProgram, errorDelete] = useDelete(url); + const start_date_time = new Date(program.start_date_time).toLocaleString(); + const end_date_time = new Date(program.end_date_time).toLocaleString(); + return ( + <div + data-testid="page" + css={css` + width: 100%; + height: 100%; + overflow: scroll; + `} + > + {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>} + {errorDelete} + <div + css={css` + margin: 2rem 3rem 3rem 3rem; + display: flex; + flex-direction: column; + `} + > + <div + css={css` + font-style: normal; + font-weight: 300; + font-size: 2.5rem; + line-height: 3.4rem; + `} + > + <Link to="/program" style={{ color: "#000000" }}> + <ArrowBack fontSize="large" /> + </Link> + <div + css={css` + display: inline; + margin-left: 2rem; + `} + > + KELOLA PROGRAM + </div> + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 1.8rem; + margin-top: 1rem; + `} + > + <LinkYellow to="/program/tambah">TAMBAH</LinkYellow> + <LinkYellow className="ml-2" to="/program"> + LIHAT + </LinkYellow> + </div> + <div className="container-fluid px-0"> + <div className="row mt-2"> + <div className="col-sm-4 mt-2"> + <img + alt={program.name} + className="img-fluid" + src={program.poster_image} + /> + </div> + <div className="col-sm-8"> + <div + className="program" + css={css` + font-style: normal; + font-weight: 300; + font-size: 36px; + line-height: 44px; + `} + > + {program.name} + </div> + <div + css={css` + margin-top: 1rem; + margin-bottom: 1rem; + `} + > + <div data-testid="program"> + <div + css={css` + margin-top: 1rem; + `} + > + <EventIcon style={{ fontSize: 20, color: "FFC80A" }} /> + <div + css={css` + display: inline; + margin-left: 2rem; + font-style: normal; + font-weight: normal; + font-size: 18px; + line-height: 22px; + `} + > + {start_date_time} - {end_date_time} + </div> + </div> + <div + css={css` + margin-top: 1rem; + `} + > + <LocationOnIcon style={{ fontSize: 20, color: "FFC80A" }} /> + <div + css={css` + display: inline; + margin-left: 2rem; + font-style: normal; + font-weight: normal; + font-size: 18px; + line-height: 22px; + `} + > + {program.location} + </div> + </div> + <div + css={css` + margin-top: 1rem; + `} + > + <PersonIcon style={{ fontSize: 20, color: "FFC80A" }} /> + <div + css={css` + display: inline; + margin-left: 2rem; + font-style: normal; + font-weight: normal; + font-size: 18px; + line-height: 22px; + `} + > + {program.speaker} + </div> + </div> + <div + css={css` + margin-top: 2rem; + margin-bottom: 1rem; + font-style: normal; + font-weight: normal; + font-size: 24px; + line-height: 22px; + `} + > + Deskripsi + </div> + <div>{program.description}</div> + </div> + </div> + <div className="container-fluid row"> + <div className="col-6"> + <div + css={css` + flex-grow: 1; + `} + > + <LinkYellow + css={css` + border: 3px solid #3c8dbc; + `} + to="ubah" + > + UBAH + </LinkYellow> + </div> + </div> + <div className="col-6"> + <div + css={css` + flex-grow: 1; + `} + > + <ButtonDeleteStyled + data-testid="button-delete-subcategory" + onClick={deleteProgram} + > + HAPUS + </ButtonDeleteStyled> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + ); +}; + +export default DetailPengguna; diff --git a/src/page/program/EditProgram.jsx b/src/page/program/EditProgram.jsx new file mode 100644 index 0000000000000000000000000000000000000000..186a6daccfd9f7c1e3f54ef52556c9a06fa266dd --- /dev/null +++ b/src/page/program/EditProgram.jsx @@ -0,0 +1,99 @@ +import React from "react"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormProgram from "./FormProgram"; +import { ArrowBack, ErrorOutline } from "@material-ui/icons"; +import { navigate } from "@reach/router"; +import useSendData from "../../utils/useSendData"; + +const EditProgram = ({ idProgram }) => { + const url = `${process.env.REACT_APP_BASE_URL}/programs/${idProgram}/`; + const [initialData, errorState] = useFetchSingleData(url); + const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 }); + const onSubmit = (data) => { + const formData = new FormData(); + formData.append("name", data["name"]); + formData.append("description", data["description"]); + formData.append("start_date_time", data["start_date_time"]); + formData.append("end_date_time", data["end_date_time"]); + formData.append("location", data["location"]); + formData.append("speaker", data["speaker"]); + if (data["poster_image"].length !== 0) + formData.append("poster_image", data["poster_image"][0]); + send(formData); + }; + if (errorState || Object.keys(initialData).length === 0) + return ( + <div + data-testid="waiting-edit-program" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + font-size: 25px; + `} + > + Fetching data.. + </div> + ); + return ( + <div + data-testid="edit-program" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>} + <div + css={css` + display: flex; + flex-direction: row; + `} + > + <button + css={css` + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate(-1)} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + font-size: 36px; + `} + > + Edit {initialData.name} + </div> + </div> + <div + css={css` + margin-top: 2.5rem; + display: flex; + `} + > + <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} /> + <div + css={css` + font-weight: 600; + font-size: 24px; + line-height: 29px; + `} + > + Informasi Program + </div> + </div> + <FormProgram {...{ onSubmit, initialData }} /> + </div> + ); +}; + +export default EditProgram; diff --git a/src/page/program/FormProgram.jsx b/src/page/program/FormProgram.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0ed17ca6603b9c936ca200e64912637b11d15787 --- /dev/null +++ b/src/page/program/FormProgram.jsx @@ -0,0 +1,116 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { + ErrorDiv, + RowInput, + InputForm, + LabelInput, + InputSubmitForm, +} from "../../component/html/html"; +import { css } from "@emotion/core"; + +const FormProgram = ({ onSubmit, initialData = null }) => { + const { register, handleSubmit, errors } = useForm({ + defaultValues: + initialData !== null + ? { + name: initialData["name"], + description: initialData["description"], + start_date_time: initialData["start_date_time"], + end_date_time: initialData["end_date_time"], + location: initialData["location"], + speaker: initialData["speaker"], + } + : {}, + }); + return ( + <form + data-testid="form-program" + onSubmit={handleSubmit(onSubmit)} + css={css` + display: flex; + flex-direction: column; + `} + > + <RowInput> + <LabelInput htmlFor="name">Nama program </LabelInput> + <InputForm + data-testid="name-program-input" + name="name" + ref={register({ required: true })} + /> + {errors.name && <ErrorDiv>Nama program tidak boleh kosong</ErrorDiv>} + </RowInput> + <RowInput> + <LabelInput htmlFor="description">Deskripsi </LabelInput> + <InputForm + data-testid="description-program-input" + name="description" + ref={register({ required: true })} + /> + {errors.description} + </RowInput> + <RowInput> + <LabelInput htmlFor="start_date_time"> + Tanggal dan waktu mulai{" "} + </LabelInput> + <InputForm + type="datetime-local" + data-testid="start-date-time-program-input" + name="start_date_time" + ref={register({ required: false })} + /> + {errors.start_date_time} + </RowInput> + <RowInput> + <LabelInput htmlFor="end_date_time"> + Tanggal dan waktu berakhir{" "} + </LabelInput> + <InputForm + type="datetime-local" + data-testid="end-date-time-program-input" + name="end_date_time" + ref={register({ required: false })} + /> + {errors.end_date_time} + </RowInput> + <RowInput> + <LabelInput htmlFor="location">Lokasi </LabelInput> + <InputForm + data-testid="location-program-input" + name="location" + ref={register({ required: false })} + /> + {errors.location} + </RowInput> + <RowInput> + <LabelInput htmlFor="speaker">Pembicara </LabelInput> + <InputForm + data-testid="speaker-program-input" + name="speaker" + ref={register({ required: false })} + /> + {errors.name} + </RowInput> + {initialData !== null && initialData["poster_image"] != null ? ( + <img + css={css` + height: 10rem; + object-fit: contain; + `} + alt={initialData["name"]} + src={initialData["poster_image"]} + /> + ) : null} + <RowInput> + <LabelInput htmlFor="poster_image">Gambar Poster </LabelInput> + <InputForm type="file" name="poster_image" ref={register} /> + </RowInput> + <RowInput> + <InputSubmitForm type="submit" data-testid="submit-category" /> + </RowInput> + </form> + ); +}; + +export default FormProgram; diff --git a/src/page/program/ListProgram.jsx b/src/page/program/ListProgram.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f58da09e41430fdaad38c11a2b4434d3d53be52a --- /dev/null +++ b/src/page/program/ListProgram.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import TableComponent from "../../component/TableComponent"; +import { css } from "@emotion/core"; +import LinkYellow from "../../component/LinkYellow"; + +const ListProgram = () => { + const data = { + url: `${process.env.REACT_APP_BASE_URL}/programs/`, + pageDefault: 1, + searchDefault: "", + title: "", + keyValuePairs: [ + ["id", "id"], + ["name", "Nama Program"], + ], + link: "", + }; + return ( + <div + css={css` + display: flex; + flex-direction: column; + margin: 2rem 3rem 3rem 3rem; + `} + > + <div + css={css` + font-size: 35px; + `} + > + KELOLA PROGRAM + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 2rem; + margin-top: 1rem; + `} + > + <LinkYellow to="tambah">TAMBAH</LinkYellow> + <LinkYellow className="ml-2" to="/program"> + LIHAT + </LinkYellow> + </div> + <TableComponent {...data} /> + </div> + ); +}; + +export default ListProgram; diff --git a/src/page/program/TambahProgram.jsx b/src/page/program/TambahProgram.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f7ef910196b34245e8989f536a77a030d56ad335 --- /dev/null +++ b/src/page/program/TambahProgram.jsx @@ -0,0 +1,64 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormProgram from "./FormProgram"; +import LinkYellow from "../../component/LinkYellow"; +import useSendData from "../../utils/useSendData"; + +const TambahProgram = () => { + const url = `${process.env.REACT_APP_BASE_URL}/programs/`; + const [send, error] = useSendData({ + url, + method: "POST", + redirect: "/program", + }); + const onSubmit = (data) => { + const formData = new FormData(); + formData.append("name", data["name"]); + formData.append("description", data["description"]); + formData.append("start_date_time", data["start_date_time"]); + formData.append("end_date_time", data["end_date_time"]); + formData.append("location", data["location"]); + formData.append("speaker", data["speaker"]); + if (data["poster_image"].length !== 0) + formData.append("poster_image", data["poster_image"][0]); + send(formData); + }; + return ( + <div + data-testid="tambah-program" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + <div> + {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>} + </div> + + <div + css={css` + font-size: 35px; + `} + > + TAMBAH PROGRAM + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 2rem; + margin-top: 1rem; + `} + > + <LinkYellow to="/program/tambah">TAMBAH</LinkYellow> + <LinkYellow to="/program">LIHAT</LinkYellow> + </div> + <FormProgram {...{ onSubmit }} /> + </div> + ); +}; + +export default TambahProgram; diff --git a/src/page/subkategori/DetailSubkategori.jsx b/src/page/subkategori/DetailSubkategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e9e59683b743254b1c2b9591a673c6240d4375b9 --- /dev/null +++ b/src/page/subkategori/DetailSubkategori.jsx @@ -0,0 +1,150 @@ +import React, { useState } from "react"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import { css } from "@emotion/core"; +import { ButtonDeleteStyled, ErrorDiv } from "../../component/html/html"; +import TableComponent from "../../component/TableComponent"; +import LinkYellow from "../../component/LinkYellow"; +import { Link, navigate } from "@reach/router"; +import { ArrowBack } from "@material-ui/icons"; +import useDelete from "../../utils/useDelete"; +import { + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Button, +} from "@material-ui/core"; + +const DetailSubkategori = ({ idSubKategori }) => { + const url = `${process.env.REACT_APP_BASE_URL}/subcategories/${idSubKategori}/`; + const [subcategory, error] = useFetchSingleData(url); + const [deleteProduct, errorDelete] = useDelete(url); + const [openModal, setOpenModal] = useState(false); + const data = { + url: `${process.env.REACT_APP_BASE_URL}/products/`, + pageDefault: 1, + searchDefault: "", + title: "", + keyValuePairs: [ + ["id", "id"], + ["name", "Nama"], + ["price", "Harga"], + ["stock", "Stok"], + ], + argument: `subcategory=${idSubKategori}`, + }; + const handleClose = () => setOpenModal(false); + return ( + <div + data-testid="page-detail-subkategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + <Dialog open={openModal} onClose={handleClose}> + <DialogTitle>{`Hapus subkategori ${subcategory.name}`}</DialogTitle> + <DialogContent> + <DialogContentText> + Apakah yakin untuk menghapus subkategori {subcategory.name}? + </DialogContentText> + <DialogActions> + <Button onClick={handleClose} color="primary"> + TIDAK + </Button> + <Button + data-testid="button-delete-subcategory" + color="secondary" + onClick={() => { + deleteProduct(); + handleClose(); + }} + > + HAPUS + </Button> + </DialogActions> + </DialogContent> + </Dialog> + {error && <ErrorDiv>Error !, Please relogin..</ErrorDiv>} + {errorDelete && ( + <ErrorDiv> + Tidak dapat menghapus subkategori, mohon periksa apakah ada produk + didalam kategori ini. + </ErrorDiv> + )} + <div + css={css` + display: flex; + flex-direction: column; + `} + > + <button + css={css` + align-self: start; + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate("/subkategori")} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + font-size: 2rem; + display: flex; + flex-direction: row; + align-items: baseline; + `} + > + <div + css={css` + flex-grow: 2; + `} + > + Subkategori: {subcategory.name} + </div> + <div + css={css` + flex-grow: 1; + `} + > + <LinkYellow to="ubah">EDIT</LinkYellow> + </div> + <div + css={css` + flex-grow: 1; + `} + > + <ButtonDeleteStyled + data-testid="button-delete-subcategory-modal" + onClick={() => setOpenModal(true)} + > + HAPUS + </ButtonDeleteStyled> + </div> + </div> + <div + css={css` + font-size: 1.5rem; + `} + > + Kategori: + <span> + <Link to={`/kategori/${subcategory.category}`}> + {subcategory.category_name} + </Link> + </span> + </div> + </div> + <TableComponent {...data} /> + </div> + ); +}; + +export default DetailSubkategori; diff --git a/src/page/subkategori/EditSubkategori.jsx b/src/page/subkategori/EditSubkategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..72c3419153ec8dd2c3fd09079e17a6b7a4b86ff2 --- /dev/null +++ b/src/page/subkategori/EditSubkategori.jsx @@ -0,0 +1,94 @@ +import React from "react"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormSubkategori from "./FormSubkategori"; +import { ArrowBack, ErrorOutline } from "@material-ui/icons"; +import { navigate } from "@reach/router"; +import useSendData from "../../utils/useSendData"; + +const EditSubkategori = ({ idSubKategori }) => { + const url = `${process.env.REACT_APP_BASE_URL}/subcategories/${idSubKategori}/`; + const [initialData, errorState] = useFetchSingleData(url); + const [send, error] = useSendData({ url, method: "PATCH", redirect: -1 }); + const onSubmit = (data) => { + const formData = new FormData(); + formData.append("name", data["name"]); + formData.append("category", data["category"]); + if (data["image"].length !== 0) formData.append("image", data["image"][0]); + send(formData); + }; + if (errorState || Object.keys(initialData).length === 0) + return ( + <div + data-testid="waiting-edit-subkategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + font-size: 25px; + `} + > + Fetching data.. + </div> + ); + return ( + <div + data-testid="edit-subkategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + {error && <ErrorDiv>Error !, Data tidak dapat disimpan</ErrorDiv>} + <div + css={css` + display: flex; + flex-direction: row; + `} + > + <button + css={css` + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate(-1)} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + font-size: 36px; + `} + > + Edit {initialData.name} + </div> + </div> + <div + css={css` + margin-top: 2.5rem; + display: flex; + `} + > + <ErrorOutline style={{ fontSize: 28, color: "FFC80A" }} /> + <div + css={css` + font-weight: 600; + font-size: 24px; + line-height: 29px; + `} + > + Informasi Subkategori + </div> + </div> + <FormSubkategori {...{ onSubmit, initialData }} /> + </div> + ); +}; + +export default EditSubkategori; diff --git a/src/page/subkategori/FormSubkategori.jsx b/src/page/subkategori/FormSubkategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3389cd54e043fe3d9ff000305f1030523e13742c --- /dev/null +++ b/src/page/subkategori/FormSubkategori.jsx @@ -0,0 +1,94 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import useFetchList from "../../utils/useFetchList"; +import { + ErrorDiv, + RowInput, + InputForm, + LabelInput, + InputSelectForm, + InputSubmitForm, +} from "../../component/html/html"; +import { css } from "@emotion/core"; + +const FormSubkategori = ({ onSubmit, initialData = null }) => { + const { register, handleSubmit, errors } = useForm({ + defaultValues: + initialData !== null + ? { name: initialData["name"], category: initialData["category"] } + : {}, + }); + const url = `${process.env.REACT_APP_BASE_URL}/categories/`; + const [results, errorState, , , , , ,] = useFetchList([ + url, + null, + null, + null, + null, + ]); + return ( + <form + data-testid="form-subcategory" + onSubmit={handleSubmit(onSubmit)} + css={css` + display: flex; + flex-direction: column; + `} + > + <div> + {errorState && ( + <ErrorDiv>Error loading form !, Please relogin..</ErrorDiv> + )} + </div> + <RowInput> + <LabelInput htmlFor="name">Nama subkategori </LabelInput> + <InputForm + data-testid="name-subkategori-input" + name="name" + ref={register({ required: true })} + /> + {errors.name && ( + <ErrorDiv>Nama subkategori tidak boleh kosong</ErrorDiv> + )} + </RowInput> + {results === undefined || Object.keys(results).length === 0 ? null : ( + <RowInput> + <LabelInput htmlFor="name">Kategori </LabelInput> + <InputSelectForm + data-testid="category-subcategory-input" + name="category" + ref={register({ required: true })} + > + {results.map((item) => ( + <option key={item.id} value={item.id}> + {item.name} + </option> + ))} + {errors.category && ( + <ErrorDiv>Kategori tidak boleh kosong</ErrorDiv> + )} + </InputSelectForm> + </RowInput> + )} + {initialData !== null && initialData["image"] != null ? ( + <img + css={css` + height: 10rem; + object-fit: contain; + `} + alt={initialData["name"]} + src={initialData["image"]} + /> + ) : null} + <RowInput> + <LabelInput htmlFor="gambar">Foto subkategori </LabelInput> + <InputForm type="file" name="image" ref={register} /> + </RowInput> + <RowInput> + <InputSubmitForm type="submit" data-testid="submit-subcategory" /> + </RowInput> + </form> + ); +}; + +export default FormSubkategori; diff --git a/src/page/subkategori/ListSubkategori.jsx b/src/page/subkategori/ListSubkategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2d4178c32ca141470cbcd5cbe9e1c325756c7676 --- /dev/null +++ b/src/page/subkategori/ListSubkategori.jsx @@ -0,0 +1,51 @@ +import React from "react"; +import TableComponent from "../../component/TableComponent"; +import { css } from "@emotion/core"; +import LinkYellow from "../../component/LinkYellow"; + +const ListSubkategori = () => { + const data = { + url: `${process.env.REACT_APP_BASE_URL}/subcategories/`, + pageDefault: 1, + searchDefault: "", + title: "", + keyValuePairs: [ + ["id", "id"], + ["name", "Nama"], + ["category_name", "Category"], + ], + link: "", + }; + return ( + <div + css={css` + display: flex; + flex-direction: column; + margin: 2rem 3rem 3rem 3rem; + `} + > + <div + css={css` + font-size: 35px; + `} + > + KELOLA SUBKATEGORI + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 2rem; + margin-top: 1rem; + `} + > + <LinkYellow to="tambah">TAMBAH</LinkYellow> + <LinkYellow to="/subkategori">LIHAT</LinkYellow> + </div> + <TableComponent {...data} /> + </div> + ); +}; + +export default ListSubkategori; diff --git a/src/page/subkategori/TambahSubkategori.jsx b/src/page/subkategori/TambahSubkategori.jsx new file mode 100644 index 0000000000000000000000000000000000000000..daf70d0c506d756bea8ab1cc51be88c3e39bd14c --- /dev/null +++ b/src/page/subkategori/TambahSubkategori.jsx @@ -0,0 +1,63 @@ +import React from "react"; +import { css } from "@emotion/core"; +import { ErrorDiv } from "../../component/html/html"; +import FormSubkategori from "./FormSubkategori"; +import LinkYellow from "../../component/LinkYellow"; +import useSendData from "../../utils/useSendData"; + +const TambahSubkategori = () => { + const url = `${process.env.REACT_APP_BASE_URL}/subcategories/`; + const [send, error] = useSendData({ + url, + method: "POST", + redirect: "/subkategori", + }); + const onSubmit = (data) => { + const formData = new FormData(); + formData.append("name", data["name"]); + formData.append("category", data["category"]); + if (data["image"].length !== 0) formData.append("image", data["image"][0]); + send(formData); + }; + return ( + <div + data-testid="tambah-subkategori" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + `} + > + <div> + {error && ( + <ErrorDiv> + Error !, Data tidak dapat disimpan, pastikan nama subkategori unik. + </ErrorDiv> + )} + </div> + + <div + css={css` + font-size: 35px; + `} + > + TAMBAH SUBKATEGORI + </div> + <div + css={css` + width: 35%; + display: flex; + flex-direction: row; + margin-bottom: 2rem; + margin-top: 1rem; + `} + > + <LinkYellow to="/subkategori/tambah">TAMBAH</LinkYellow> + <LinkYellow to="/subkategori">LIHAT</LinkYellow> + </div> + <FormSubkategori {...{ onSubmit }} /> + </div> + ); +}; + +export default TambahSubkategori; diff --git a/src/page/transaksi/DetailTransaksi.jsx b/src/page/transaksi/DetailTransaksi.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e151cba85cc7fabe89129ffb9f7d19e3e0ed839c --- /dev/null +++ b/src/page/transaksi/DetailTransaksi.jsx @@ -0,0 +1,382 @@ +import React, { useState } from "react"; +import useFetchSingleData from "../../utils/useFetchSingleData"; +import { css } from "@emotion/core"; +import { ArrowBack, Contacts, Phone, Photo } from "@material-ui/icons"; +import { Link, navigate } from "@reach/router"; +import PersonIcon from "@material-ui/icons/Person"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import Paper from "@material-ui/core/Paper"; +import Status from "../../component/Status"; +import { stringToCurrency, stringToDate } from "../../component/TableUtils"; +import { ErrorDiv } from "../../component/html/html"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import FormStatus from "./FormStatus"; +const DetailTransaksi = ({ idTransaksi }) => { + const url = `${process.env.REACT_APP_BASE_URL}/transactions/${idTransaksi}`; + const [transaction, error] = useFetchSingleData(url); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleClickOpen = () => { + setDialogOpen(true); + }; + + const handleClose = () => { + setDialogOpen(false); + }; + + if (Object.keys(transaction).length === 0) + return ( + <div + data-testid="waiting-detail-transaksi" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + font-size: 25px; + `} + > + {error && <ErrorDiv>Something error</ErrorDiv>} + Fetching data.. + </div> + ); + return ( + <div + data-testid="page-detail-transaksi" + css={css` + display: flex; + margin: 2rem 3rem 3rem 3rem; + flex-direction: column; + justify-content: space-around; + height: 125vh; + `} + > + {error && <ErrorDiv>Something error</ErrorDiv>} + <button + css={css` + align-self: start; + background-color: Transparent; + background-repeat: no-repeat; + border: none; + cursor: pointer; + overflow: hidden; + outline: none; + `} + onClick={() => navigate(-1)} + > + <ArrowBack fontSize="large" /> + </button> + <div + css={css` + font-size: 1rem; + `} + > + ADMIN - KELOLA TRANSAKSI + </div> + <div + css={css` + font-size: 1.5rem; + display: flex; + align-content: space-between; + `} + > + <div + css={css` + display: flex; + `} + > + <div + css={css` + margin-right: 1rem; + `} + > + Transaction code: + </div> + <div>{transaction.transaction_number}</div> + </div> + {transaction.transaction_status !== "006" && + transaction.transaction_status !== "007" && ( + <FormStatus + {...{ + idTransaksi, + defaultStatus: transaction.transaction_status, + paymentMethod: transaction.payment_method, + }} + /> + )} + </div> + <div + css={css` + font-size: 1.5rem; + display: flex; + `} + > + <div + css={css` + margin-right: 1rem; + `} + > + Status:{" "} + </div> + <Status + label={transaction.readable_transaction_status} + status={transaction.transaction_status} + /> + </div> + <div + css={css` + font-size: 1.2rem; + display: flex; + `} + > + <div + css={css` + margin-right: 0.5rem; + `} + > + Date created:{" "} + </div> + <div>{stringToDate(transaction.created_at)}</div> + </div> + <div + css={css` + font-size: 1.2rem; + display: flex; + `} + > + <div + css={css` + margin-right: 0.5rem; + `} + > + Date updated:{" "} + </div> + <div>{stringToDate(transaction.updated_at)}</div> + </div> + <div + css={css` + display: flex; + font-size: 1.2rem; + align-items: baseline; + `} + > + <PersonIcon /> + <Link to={`/pengguna/${transaction.user}`}> + {transaction.user_full_name} / {transaction.user_username} + </Link> + </div> + + <div + css={css` + display: flex; + font-size: 1.2rem; + align-items: baseline; + `} + > + <Phone /> + <div>{transaction.user_phone_number}</div> + </div> + <div + css={css` + display: flex; + flex-direction: column; + `} + > + <div + css={css` + font-size: 1.2rem; + `} + > + <Contacts /> + Alamat Pengiriman + <div>{`${transaction.shipping_address}, + RT ${transaction.shipping_neighborhood}, + RW ${transaction.shipping_hamlet}, + Kelurahan ${transaction.shipping_urban_village}, + Kecamatan ${transaction.shipping_sub_district}`}</div> + </div> + </div> + <TableContainer component={Paper}> + <Table aria-label="simple table"> + <TableHead> + <TableRow> + <TableCell>Product Code</TableCell> + <TableCell>Product Name</TableCell> + <TableCell>Quantity</TableCell> + <TableCell>Product Price</TableCell> + <TableCell>Subtotal</TableCell> + </TableRow> + </TableHead> + <TableBody> + {transaction.transaction_items.map((row) => ( + <TableRow key={row.id}> + <TableCell component="th" scope="row"> + <Link to={`/produk/${row.product}`}>{row.product_code}</Link> + </TableCell> + <TableCell>{row.product_name}</TableCell> + <TableCell>{row.quantity}</TableCell> + <TableCell>{stringToCurrency(row.product_price)}</TableCell> + <TableCell> + {stringToCurrency( + `${Number.parseFloat(row.product_price) * row.quantity}` + )} + </TableCell> + </TableRow> + ))} + <TableRow> + <TableCell rowSpan={3} colSpan={3} /> + <TableCell align="left" colSpan={1}> + Donasi + </TableCell> + <TableCell align="left"> + {stringToCurrency(transaction.donation)} + </TableCell> + </TableRow> + <TableRow> + <TableCell align="left" colSpan={1}> + Shipping cost + </TableCell> + <TableCell align="left"> + {stringToCurrency(transaction.shipping_costs)} + </TableCell> + </TableRow> + <TableRow> + <TableCell colSpan={1} align="left"> + Total + </TableCell> + <TableCell align="left"> + {stringToCurrency(transaction.subtotal)} + </TableCell> + </TableRow> + </TableBody> + </Table> + </TableContainer> + <div + css={css` + display: flex; + flex-direction: column; + `} + > + {" "} + <div + css={css` + font-size: 1.5rem; + `} + > + Pembayaran + </div> + {transaction.payment_method === "COD" && ( + <div + css={css` + font-size: 1.3rem; + `} + > + Cash On Delivery + </div> + )} + {transaction.payment_method === "TRF" && ( + <div + css={css` + font-size: 1.3rem; + `} + > + Transfer + <div> + {transaction.proof_of_payment === null ? ( + <div>User belum mengirimkan bukti pembayaran</div> + ) : ( + <div + css={css` + display: flex; + flex-direction: column; + `} + > + <div + css={css` + display: flex; + font-size: 1.2rem; + align-items: baseline; + `} + > + <div + css={css` + margin-right: 1rem; + `} + > + Sender name:{" "} + </div> + <div>{transaction.user_bank_account_name}</div> + </div> + <div + css={css` + display: flex; + font-size: 1.2rem; + align-items: baseline; + `} + > + <div + css={css` + margin-right: 1rem; + `} + > + Sender account number:{" "} + </div> + <div>{transaction.user_bank_account_number}</div> + </div> + <Button + onClick={handleClickOpen} + variant="contained" + color="primary" + size="medium" + startIcon={<Photo />} + data-testid="button-see-proof" + > + Bukti + </Button> + </div> + )} + </div> + </div> + )} + </div> + <Dialog + maxWidth="xl" + open={dialogOpen} + onClose={handleClose} + aria-labelledby="max-width-dialog-title" + > + <DialogTitle id="max-width-dialog-title">Bukti Pembayaran</DialogTitle> + <DialogContent> + <img + css={css` + height: 80vh; + width: 80vw; + object-fit: contain; + `} + src={transaction.proof_of_payment} + alt="Bukti Pembayaran" + /> + </DialogContent> + <DialogActions> + <Button + data-testid="button-close-proof" + onClick={handleClose} + color="primary" + > + Close + </Button> + </DialogActions> + </Dialog> + </div> + ); +}; + +export default DetailTransaksi; diff --git a/src/page/transaksi/FormStatus.jsx b/src/page/transaksi/FormStatus.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e97a90fae126771ab1365aa230e19c86d791fa7d --- /dev/null +++ b/src/page/transaksi/FormStatus.jsx @@ -0,0 +1,77 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import useSendData from "../../utils/useSendData"; +import { + ErrorDiv, + InputSelectForm, + LabelInput, + RowInput, +} from "../../component/html/html"; +import { css } from "@emotion/core"; +import Button from "@material-ui/core/Button"; +import { Save } from "@material-ui/icons"; + +const FormStatus = ({ idTransaksi, defaultStatus, paymentMethod }) => { + const url = `${process.env.REACT_APP_BASE_URL}/transactions/${idTransaksi}`; + const { register, handleSubmit } = useForm({ + defaultValues: { + transaction_status: defaultStatus, + }, + }); + const [send, errorSend] = useSendData({ + url, + header: { "Content-Type": "application/json" }, + method: "PUT", + redirect: `/transaksi/`, + }); + + const onSubmit = (data) => { + send(JSON.stringify({ ...data })); + }; + return ( + <div data-testid="form-status"> + {errorSend && <ErrorDiv>Status transaksi tidak dapat disimpan</ErrorDiv>} + <form + onSubmit={handleSubmit(onSubmit)} + css={css` + display: flex; + `} + > + <RowInput> + <LabelInput + htmlFor="status" + css={css` + margin-right: 1rem; + `} + /> + <InputSelectForm + data-testid="dropdown-status" + id="status" + ref={register({ required: true })} + name="transaction_status" + > + {paymentMethod === "TRF" && ( + <option value="001">Waiting for proof of payment</option> + )} + <option value="002">Waiting for seller confirmation</option> + <option value="003">In process</option> + <option value="004">Being shipped</option> + <option value="005">Completed</option> + <option value="006">Canceled</option> + </InputSelectForm> + </RowInput> + <Button + data-testid="button-submit-status" + type="submit" + variant="contained" + color="primary" + startIcon={<Save />} + > + Simpan + </Button> + </form> + </div> + ); +}; + +export default FormStatus; diff --git a/src/page/transaksi/ListTransaksi.jsx b/src/page/transaksi/ListTransaksi.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d44596307fcee537fa05f0756dde652a59bd6c9e --- /dev/null +++ b/src/page/transaksi/ListTransaksi.jsx @@ -0,0 +1,86 @@ +import React from "react"; +import TableComponent from "../../component/TableComponent"; +import { css } from "@emotion/core"; +import LinkYellow from "../../component/LinkYellow"; +import { + stringToCurrency, + stringToDate, + transactionToColoredStatus, +} from "../../component/TableUtils"; + +const ListTransaksi = () => { + const data = { + url: `${process.env.REACT_APP_BASE_URL}/transactions/`, + pageDefault: 1, + title: "", + keyValuePairs: [ + ["id", "id"], + ["transaction_number", "ID Transaksi"], + ["user_username", "Username"], + ["created_at", "Tanggal Pembuatan", stringToDate], + ["updated_at", "Tanggal Update", stringToDate], + ["", "Status", transactionToColoredStatus], + ["subtotal", "Total", stringToCurrency], + ], + link: "/transaksi/", + filter: [ + ["updated_at_date_range_after", "Updated from"], + ["updated_at_date_range_before", "Updated before"], + { + transaction_status: { + label: "Status transaksi", + choices: [ + { "001": "Waiting for proof of payment" }, + { "002": "Waiting for seller confirmation" }, + { "003": "In process" }, + { "004": "Being shipped" }, + { "005": "Completed" }, + { "006": "Canceled" }, + ], + }, + }, + ], + }; + return ( + <div + css={css` + display: flex; + flex-direction: column; + margin: 2rem 3rem 3rem 3rem; + `} + > + <div + css={css` + font-size: 35px; + `} + > + KELOLA TRANSAKSI + </div> + <div + css={css` + width: 100%; + justify-content: space-between; + display: flex; + flex-direction: row; + margin-bottom: 1.8rem; + margin-top: 1rem; + `} + > + <div + css={css` + display: flex; + width: 35%; + `} + > + <LinkYellow to="/subkategori">SUBKATEGORI</LinkYellow> + <LinkYellow className="ml-2" to="/kategori"> + KATEGORI + </LinkYellow> + </div> + </div> + <TableComponent {...data} /> + </div> + ); +}; + +export default ListTransaksi; diff --git a/src/routes.jsx b/src/routes.jsx new file mode 100644 index 0000000000000000000000000000000000000000..10f043d398785a506e0acaff47a6177a9ccb35de --- /dev/null +++ b/src/routes.jsx @@ -0,0 +1,80 @@ +import React from "react"; +import { Router } from "@reach/router"; +import ProtectedRoute from "./component/routes/ProtectedRoute"; +import Login from "./page/login/Login"; +import UnauthenticatedRoute from "./component/routes/UnauthenticatedRoute"; +import DetailPengguna from "./page/pengguna/DetailPengguna"; +import ListPengguna from "./page/pengguna/ListPengguna"; +import ListSubkategori from "./page/subkategori/ListSubkategori"; +import DetailSubkategori from "./page/subkategori/DetailSubkategori"; +import TambahSubkategori from "./page/subkategori/TambahSubkategori"; +import EditSubkategori from "./page/subkategori/EditSubkategori"; +import ListProduk from "./page/produk/ListProduk"; +import DetailProduk from "./page/produk/DetailProduk"; +import TambahProduk from "./page/produk/TambahProduk"; +import EditProduk from "./page/produk/EditProduk"; + +import ListKategori from "./page/kategori/ListKategori"; +import DetailKategori from "./page/kategori/DetailKategori"; +import TambahKategori from "./page/kategori/TambahKategori"; +import EditKategori from "./page/kategori/EditKategori"; +import ListProgram from "./page/program/ListProgram"; +import DetailProgram from "./page/program/DetailProgram"; +import TambahProgram from "./page/program/TambahProgram"; +import EditProgram from "./page/program/EditProgram"; + +import ListTransaksi from "./page/transaksi/ListTransaksi"; +import DetailTransaksi from "./page/transaksi/DetailTransaksi"; + +const Placeholder = ({ children }) => children; + +const Routes = () => { + return ( + <Router> + <Placeholder path="/produk"> + <ProtectedRoute path="/" component={ListProduk} /> + <ProtectedRoute path="tambah" component={TambahProduk} /> + <ProtectedRoute path=":productId" component={DetailProduk} /> + <ProtectedRoute path=":productId/ubah" component={EditProduk} /> + </Placeholder> + + <Placeholder path="/pengguna"> + <ProtectedRoute path="/" component={ListPengguna} /> + <ProtectedRoute path=":userId" component={DetailPengguna} /> + </Placeholder> + + <Placeholder path="subkategori"> + <ProtectedRoute path="/" component={ListSubkategori} /> + <ProtectedRoute path="tambah" component={TambahSubkategori} /> + <ProtectedRoute path=":idSubKategori" component={DetailSubkategori} /> + <ProtectedRoute + path=":idSubKategori/ubah" + component={EditSubkategori} + /> + </Placeholder> + + <Placeholder path="kategori"> + <ProtectedRoute path="/" component={ListKategori} /> + <ProtectedRoute path="tambah" component={TambahKategori} /> + <ProtectedRoute path=":idKategori" component={DetailKategori} /> + <ProtectedRoute path=":idKategori/ubah" component={EditKategori} /> + </Placeholder> + + <Placeholder path="program"> + <ProtectedRoute path="/" component={ListProgram} /> + <ProtectedRoute path="tambah" component={TambahProgram} /> + <ProtectedRoute path=":idProgram" component={DetailProgram} /> + <ProtectedRoute path=":idProgram/ubah" component={EditProgram} /> + </Placeholder> + + <Placeholder path="transaksi"> + <ProtectedRoute path="/" component={ListTransaksi} /> + <ProtectedRoute path=":idTransaksi" component={DetailTransaksi} /> + </Placeholder> + + <UnauthenticatedRoute path="/" component={Login} /> + </Router> + ); +}; + +export default Routes; diff --git a/src/store/actions/action_types.js b/src/store/actions/action_types.js new file mode 100644 index 0000000000000000000000000000000000000000..33f613b1530885b7144a3ef5f90a146ef4e3a79d --- /dev/null +++ b/src/store/actions/action_types.js @@ -0,0 +1,3 @@ +export const LOGIN = "LOGIN"; + +export const LOGOUT = "LOGOUT"; diff --git a/src/store/actions/actions.js b/src/store/actions/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..04814d1630ef759ced6d95c7ce2747f87849062e --- /dev/null +++ b/src/store/actions/actions.js @@ -0,0 +1,14 @@ +import * as ACTION_TYPES from "./action_types"; + +export const login = (profile) => { + return { + type: ACTION_TYPES.LOGIN, + profile: profile, + }; +}; + +export const logout = () => { + return { + type: ACTION_TYPES.LOGOUT, + }; +}; diff --git a/src/store/reducers/auth_reducer.js b/src/store/reducers/auth_reducer.js new file mode 100644 index 0000000000000000000000000000000000000000..c609d9f6856da368dd9f2ebed9b535e423028eb0 --- /dev/null +++ b/src/store/reducers/auth_reducer.js @@ -0,0 +1,37 @@ +import * as ACTION_TYPES from "../actions/action_types"; + +export const initialState = () => { + if (localStorage.getItem("auth")) { + return JSON.parse(localStorage.getItem("auth")); + } + return { + is_authenticated: false, + profile: null, + }; +}; + +const persistState = (profile) => { + const state = { + is_authenticated: true, + profile: profile, + }; + localStorage.setItem("auth", JSON.stringify(state)); + return state; +}; + +const AuthReducer = (state, action) => { + switch (action.type) { + case ACTION_TYPES.LOGIN: + return persistState(action.profile); + case ACTION_TYPES.LOGOUT: + window.localStorage.clear(); + return { + is_authenticated: false, + profile: null, + }; + default: + return state; + } +}; + +export default AuthReducer; diff --git a/src/utils/contex.js b/src/utils/contex.js new file mode 100644 index 0000000000000000000000000000000000000000..6b6b84fdda60ef34f30e3bde2c9b9a565da3e62a --- /dev/null +++ b/src/utils/contex.js @@ -0,0 +1,7 @@ +import { createContext, useContext } from "react"; + +export const useAuthContext = () => useContext(AuthContext); + +const AuthContext = createContext(null); + +export default AuthContext; diff --git a/src/utils/useDelete.jsx b/src/utils/useDelete.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7d034291f157b8ba89b182286e70efb38bc55df3 --- /dev/null +++ b/src/utils/useDelete.jsx @@ -0,0 +1,33 @@ +import { useCallback, useState } from "react"; +import { useAuthContext } from "./contex"; +import { navigate } from "@reach/router"; +import { trackPromise } from "react-promise-tracker"; + +const useDelete = (url) => { + const { profile } = useAuthContext(); + const [error, setErrorState] = useState(false); + const deleteFunction = useCallback(() => { + const controller = new AbortController(); + setErrorState(false); + trackPromise( + fetch(url, { + method: "DELETE", + signal: controller.signal, + headers: { + Authorization: `Token ${profile.token}`, + }, + }) + .then((response) => { + if (response.ok) { + navigate("./"); + } else { + throw new Error("Error"); + } + }) + .catch((e) => setErrorState(!(e instanceof DOMException))) + ); + return () => controller.abort(); + }, [url, profile.token]); + return [deleteFunction, error]; +}; +export default useDelete; diff --git a/src/utils/useFetchList.jsx b/src/utils/useFetchList.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8cc283050e1044f96f85a4ffe2167836c6f49d3a --- /dev/null +++ b/src/utils/useFetchList.jsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from "react"; +import { useAuthContext } from "./contex"; +import { trackPromise } from "react-promise-tracker"; +const useFetchList = ([ + urlString, + defaultPage, + defaultSearch, + defaultSize, + argument, + filterString, +]) => { + const [statePage, setPage] = useState(defaultPage); + const [stateSearch, setStateSearch] = useState(defaultSearch); + const [stateSize, setPageSize] = useState(defaultSize); + const [results, setResults] = useState([]); + const [errorState, setErrorState] = useState(false); + const [count, setCount] = useState(0); + const [filter, setFilter] = useState(filterString); + const { profile } = useAuthContext(); + useEffect(() => { + const controller = new AbortController(); + setErrorState(false); + let url = urlString.concat("?"); + url = + statePage !== null && statePage !== undefined + ? url.concat(`page=${statePage}&`) + : url; + url = + stateSize !== null && stateSize !== undefined + ? url.concat(`page_size=${stateSize}&`) + : url; + url = + argument !== null && argument !== undefined + ? url.concat(`${argument}&`) + : url; + url = + filter !== null && filter !== undefined ? url.concat(`${filter}&`) : url; + url = + stateSearch !== null && stateSearch !== undefined + ? url.concat(`search=${stateSearch}`) + : url; + trackPromise( + fetch(url, { + method: "GET", + signal: controller.signal, + headers: { + Authorization: `Token ${profile.token}`, + }, + }) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + throw new Error("Error"); + } + }) + .then((result) => { + setResults(result["results"]); + setCount(result["count"]); + }) + .catch((e) => setErrorState(!(e instanceof DOMException))) + ); + return () => controller.abort(); + }, [ + statePage, + stateSearch, + stateSize, + argument, + urlString, + profile.token, + filter, + ]); + return [ + results, + errorState, + count, + statePage, + stateSize, + setPage, + setStateSearch, + setPageSize, + setFilter, + ]; +}; +export default useFetchList; diff --git a/src/utils/useFetchSingleData.jsx b/src/utils/useFetchSingleData.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d53256cf15aa3b66b5a02add349aa31bf75d77d5 --- /dev/null +++ b/src/utils/useFetchSingleData.jsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { useAuthContext } from "./contex"; +import { trackPromise } from "react-promise-tracker"; +const useFetchSingleData = (url) => { + const [data, setData] = useState({}); + const [error, setError] = useState(false); + const { profile } = useAuthContext(); + useEffect(() => { + setError(false); + const controller = new AbortController(); + trackPromise( + fetch(url, { + method: "GET", + signal: controller.signal, + headers: { + Authorization: `Token ${profile.token}`, + }, + }) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + throw new Error("Error"); + } + }) + .then((result) => { + setData(result); + }) + .catch((e) => setError(!(e instanceof DOMException))) + ); + return () => controller.abort; + }, [url, profile.token]); + return [data, error]; +}; + +export default useFetchSingleData; diff --git a/src/utils/useSendData.jsx b/src/utils/useSendData.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a2465eaf1c2b18e5341d3776114d9e376b4ed253 --- /dev/null +++ b/src/utils/useSendData.jsx @@ -0,0 +1,39 @@ +import { useAuthContext } from "./contex"; +import { useCallback, useState } from "react"; +import { trackPromise } from "react-promise-tracker"; +import { navigate } from "@reach/router"; + +const useSendData = ({ url, header = {}, redirect = "./", method }) => { + const { profile } = useAuthContext(); + const [error, setErrorState] = useState(false); + const sendFunction = useCallback( + (data) => { + setErrorState(false); + const controller = new AbortController(); + trackPromise( + fetch(url, { + method: method, + signal: controller.signal, + headers: { + Authorization: `Token ${profile.token}`, + ...header, + }, + body: data, + }) + .then((response) => { + if (response.ok) { + setErrorState(false); + navigate(redirect); + } else { + throw new Error("Error"); + } + }) + .catch((e) => setErrorState(!(e instanceof DOMException))) + ); + return () => controller.abort(); + }, + [url, header, profile.token] + ); + return [sendFunction, error]; +}; +export default useSendData;