├── .prettierignore ├── .githooks └── pre-commit ├── business-dashboard ├── src │ ├── vite-env.d.ts │ ├── assets │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── user │ │ │ │ ├── profile.webp │ │ │ │ └── business.webp │ │ └── css │ │ │ └── index.css │ ├── config │ │ ├── const.ts │ │ └── utils.ts │ ├── components │ │ ├── Error │ │ │ └── 404.tsx │ │ ├── Breadcrumb.tsx │ │ ├── Alerts │ │ │ ├── Success.tsx │ │ │ └── Error.tsx │ │ ├── Header.tsx │ │ ├── Dropdown │ │ │ └── DropdownUser.tsx │ │ ├── Sidebar │ │ │ └── Sidebar.tsx │ │ ├── Categories │ │ │ ├── Create.tsx │ │ │ ├── Edit.tsx │ │ │ └── index.tsx │ │ └── Users │ │ │ └── index.tsx │ ├── pages │ │ └── Authentication │ │ │ ├── PrivateRoute.tsx │ │ │ └── SignIn.tsx │ ├── store │ │ ├── categories.ts │ │ └── index.ts │ ├── graphQL │ │ ├── queries.tsx │ │ └── mutations.tsx │ ├── main.tsx │ ├── hooks │ │ └── useLocalStorage.tsx │ ├── layout │ │ └── DefaultLayout.tsx │ └── App.tsx ├── postcss.config.cjs ├── public │ ├── favicon.ico │ ├── logo192.png │ └── logo512.png ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── vite.config.ts ├── package.json └── tailwind.config.cjs ├── react-frontend ├── src │ ├── vite-env.d.ts │ ├── assets │ │ ├── css │ │ │ └── index.css │ │ └── images │ │ │ └── registration-800w.webp │ ├── config │ │ ├── const.ts │ │ └── utils.ts │ ├── App.tsx │ ├── graphQL │ │ └── mutations.tsx │ ├── main.tsx │ └── components │ │ └── modal.tsx ├── postcss.config.js ├── public │ └── favicon.ico ├── tailwind.config.js ├── vite.config.ts ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── tsconfig.json └── package.json ├── .gitignore ├── backend ├── jest.config.ts ├── src │ ├── exceptions │ │ ├── NotFoundException.ts │ │ ├── ForbiddenException.ts │ │ ├── BadRequestException.ts │ │ ├── NotAcceptableException.ts │ │ ├── ServerErrorException.ts │ │ ├── HttpException.ts │ │ ├── UnprocessableEntityException.ts │ │ └── UnauthorizedException.ts │ ├── business_type │ │ ├── resolver.ts │ │ ├── model.ts │ │ ├── api-doc.md │ │ └── __tests__ │ │ │ └── types.test.ts │ ├── config │ │ ├── const.config.ts │ │ └── db.config.ts │ ├── category │ │ ├── model.ts │ │ ├── resolver.ts │ │ ├── api-doc.md │ │ └── __tests__ │ │ │ └── category.test.ts │ ├── admin │ │ ├── model.ts │ │ ├── resolver.ts │ │ ├── api-doc.md │ │ └── __tests__ │ │ │ └── admin.test.ts │ ├── util │ │ └── handlers.util.ts │ ├── business │ │ ├── resolver.ts │ │ ├── api-doc.md │ │ ├── model.ts │ │ └── __tests__ │ │ │ └── business.test.ts │ ├── index.ts │ ├── business_user │ │ ├── model.ts │ │ ├── api-doc.md │ │ └── resolver.ts │ └── user │ │ ├── model.ts │ │ ├── resolver.ts │ │ └── api-doc.md ├── tsconfig.json ├── README.md └── package.json ├── react-admin ├── src │ ├── lib.d.ts │ ├── assets │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── user │ │ │ │ ├── profile.webp │ │ │ │ └── business.webp │ │ └── css │ │ │ └── index.css │ ├── react-app-env.d.ts │ ├── pages │ │ ├── Dashboard │ │ │ └── Home.tsx │ │ └── Authentication │ │ │ ├── PrivateRoute.tsx │ │ │ └── SignIn.tsx │ ├── config │ │ ├── const.ts │ │ └── utils.ts │ ├── hooks │ │ ├── useColorMode.tsx │ │ └── useLocalStorage.tsx │ ├── main.tsx │ ├── components │ │ ├── Breadcrumb.tsx │ │ ├── Alerts │ │ │ ├── Success.tsx │ │ │ └── Error.tsx │ │ ├── DarkModeSwitcher.tsx │ │ ├── Email │ │ │ └── approval.tsx │ │ ├── Header.tsx │ │ ├── Dropdown │ │ │ └── DropdownUser.tsx │ │ └── Sidebar │ │ │ └── Sidebar.tsx │ ├── graphQL │ │ ├── queries.tsx │ │ └── mutations.tsx │ ├── App.tsx │ └── layout │ │ └── DefaultLayout.tsx ├── postcss.config.cjs ├── public │ └── favicon.ico ├── vite.config.ts ├── .gitignore ├── index.html ├── tsconfig.json ├── package.json └── tailwind.config.cjs ├── .github ├── workflows │ └── build.yml └── pull_request_template.md ├── LICENSE.md └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npx prettier -w . && git add . -------------------------------------------------------------------------------- /business-dashboard/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /react-frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .env 5 | coverage 6 | schema.gql -------------------------------------------------------------------------------- /backend/jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /react-frontend/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /react-admin/src/lib.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /react-admin/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /react-frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /react-admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/react-admin/public/favicon.ico -------------------------------------------------------------------------------- /business-dashboard/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /react-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/react-frontend/public/favicon.ico -------------------------------------------------------------------------------- /business-dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/business-dashboard/public/favicon.ico -------------------------------------------------------------------------------- /business-dashboard/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/business-dashboard/public/logo192.png -------------------------------------------------------------------------------- /business-dashboard/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/business-dashboard/public/logo512.png -------------------------------------------------------------------------------- /react-frontend/src/config/const.ts: -------------------------------------------------------------------------------- 1 | const messages = { 2 | ERROR: "Something went wrong!! Please try again.", 3 | }; 4 | 5 | export { messages }; 6 | -------------------------------------------------------------------------------- /react-admin/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/react-admin/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /react-admin/src/assets/images/user/profile.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/react-admin/src/assets/images/user/profile.webp -------------------------------------------------------------------------------- /business-dashboard/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/business-dashboard/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /react-admin/src/assets/images/user/business.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/react-admin/src/assets/images/user/business.webp -------------------------------------------------------------------------------- /react-admin/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | declare module "*.svg"; 3 | declare module "*.jpeg"; 4 | declare module "*.jpg"; 5 | declare module "*.webp"; 6 | -------------------------------------------------------------------------------- /business-dashboard/src/assets/images/user/profile.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/business-dashboard/src/assets/images/user/profile.webp -------------------------------------------------------------------------------- /business-dashboard/src/assets/images/user/business.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/business-dashboard/src/assets/images/user/business.webp -------------------------------------------------------------------------------- /react-frontend/src/assets/images/registration-800w.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canopas/fullstack-graphql-react-starter-kit/HEAD/react-frontend/src/assets/images/registration-800w.webp -------------------------------------------------------------------------------- /react-admin/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /react-frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Registration from "./components/registration"; 3 | 4 | const App: React.FC = () => { 5 | return ; 6 | }; 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /react-frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /business-dashboard/src/config/const.ts: -------------------------------------------------------------------------------- 1 | const roles = { 2 | ADMIN: 1, 3 | OWNER: 2, 4 | USER: 3, 5 | }; 6 | 7 | const messages = { 8 | ERROR: "Something went wrong!! Please try again.", 9 | }; 10 | 11 | export { roles, messages }; 12 | -------------------------------------------------------------------------------- /react-frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | server: { 7 | port: 3000, 8 | }, 9 | plugins: [react()], 10 | }); 11 | -------------------------------------------------------------------------------- /business-dashboard/src/config/utils.ts: -------------------------------------------------------------------------------- 1 | export const validEmail = (email: string) => { 2 | let emailRegx = 3 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 4 | return emailRegx.test(email); 5 | }; 6 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Error/404.tsx: -------------------------------------------------------------------------------- 1 | const PageNotFound = () => { 2 | return ( 3 |
4 |

404

5 |
Page Not Found
6 |
7 | ); 8 | }; 9 | 10 | export default PageNotFound; 11 | -------------------------------------------------------------------------------- /react-frontend/src/graphQL/mutations.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | const CREATE_BUSINESS_MUTATION = gql` 4 | mutation createUser($data: UserInput!) { 5 | createUser(data: $data) { 6 | name 7 | email 8 | phone 9 | city 10 | } 11 | } 12 | `; 13 | 14 | export { CREATE_BUSINESS_MUTATION }; 15 | -------------------------------------------------------------------------------- /react-admin/src/pages/Dashboard/Home.tsx: -------------------------------------------------------------------------------- 1 | import DefaultLayout from "../../layout/DefaultLayout.jsx"; 2 | 3 | const Home = () => { 4 | return ( 5 | 6 |
7 |
8 | ); 9 | }; 10 | 11 | export default Home; 12 | -------------------------------------------------------------------------------- /backend/src/exceptions/NotFoundException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "./HttpException"; 2 | import { statusCodes } from "../config/const.config"; 3 | 4 | class NotFoundException extends HttpException { 5 | constructor(msg?: string, code?: string) { 6 | super(statusCodes.NOT_FOUND, msg ?? "Not found", code); 7 | } 8 | } 9 | 10 | export default NotFoundException; 11 | -------------------------------------------------------------------------------- /backend/src/exceptions/ForbiddenException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "./HttpException"; 2 | import { statusCodes } from "../config/const.config"; 3 | 4 | class ForbiddenException extends HttpException { 5 | constructor(msg?: string, code?: string) { 6 | super(statusCodes.FORBIDDEN, msg ?? "Forbidden", code); 7 | } 8 | } 9 | 10 | export default ForbiddenException; 11 | -------------------------------------------------------------------------------- /backend/src/exceptions/BadRequestException.ts: -------------------------------------------------------------------------------- 1 | import { statusCodes } from "../config/const.config"; 2 | import HttpException from "./HttpException"; 3 | 4 | class BadRequestException extends HttpException { 5 | constructor(msg?: string, code?: string) { 6 | super(statusCodes.BAD_REQUEST, msg ?? "Bad request", code); 7 | } 8 | } 9 | 10 | export default BadRequestException; 11 | -------------------------------------------------------------------------------- /react-admin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /react-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /react-admin/src/pages/Authentication/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from "react-router-dom"; 2 | import useLocalStorage from "../../hooks/useLocalStorage"; 3 | 4 | export const PrivateRoute = ({ children }: any) => { 5 | const [user, _] = useLocalStorage("user", ""); 6 | 7 | if (user !== "") { 8 | return children; 9 | } 10 | 11 | return ; 12 | }; 13 | -------------------------------------------------------------------------------- /backend/src/exceptions/NotAcceptableException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "./HttpException"; 2 | import { statusCodes } from "../config/const.config"; 3 | 4 | class NotAcceptableException extends HttpException { 5 | constructor(msg?: string, code?: string) { 6 | super(statusCodes.NOT_ACCEPTABLE, msg ?? "Not acceptable", code); 7 | } 8 | } 9 | 10 | export default NotAcceptableException; 11 | -------------------------------------------------------------------------------- /backend/src/exceptions/ServerErrorException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "./HttpException"; 2 | import { statusCodes } from "../config/const.config"; 3 | 4 | class ServerErrorException extends HttpException { 5 | constructor(msg?: string, code?: string) { 6 | super(statusCodes.SERVER_ERROR, msg ?? "Internal server error", code); 7 | } 8 | } 9 | 10 | export default ServerErrorException; 11 | -------------------------------------------------------------------------------- /business-dashboard/src/pages/Authentication/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from "react-router-dom"; 2 | import useLocalStorage from "../../hooks/useLocalStorage"; 3 | 4 | export const PrivateRoute = ({ children }: any) => { 5 | const [user, _] = useLocalStorage("user", ""); 6 | 7 | if (user !== "") { 8 | return children; 9 | } 10 | 11 | return ; 12 | }; 13 | -------------------------------------------------------------------------------- /react-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Omnidashboard 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | 4 | jobs: 5 | run_tests: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2.3.3 10 | 11 | - uses: actions/setup-go@v2 12 | with: 13 | node-version: "19" 14 | 15 | - name: Run API test 16 | run: | 17 | cd backend 18 | yarn install 19 | yarn test 20 | -------------------------------------------------------------------------------- /react-admin/src/config/const.ts: -------------------------------------------------------------------------------- 1 | const roles = { 2 | ADMIN: 1, 3 | OWNER: 2, 4 | USER: 3, 5 | }; 6 | 7 | const gender = { 8 | MALE: 1, 9 | FEMALE: 2, 10 | OTHER: 3, 11 | }; 12 | 13 | const status = { 14 | PENDING: 0, 15 | APPROVED: 1, 16 | REJECTED: 2, 17 | }; 18 | 19 | const messages = { 20 | ERROR: "Something went wrong!! Please try again.", 21 | }; 22 | 23 | export { roles, gender, status, messages }; 24 | -------------------------------------------------------------------------------- /backend/src/exceptions/HttpException.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from "graphql"; 2 | 3 | class HttpException extends GraphQLError { 4 | status: number; 5 | message: string; 6 | extensions: {}; 7 | constructor(status: number, message: string, code?: string) { 8 | super(message); 9 | 10 | this.message = message; 11 | this.extensions = { code: code, status: status }; 12 | } 13 | } 14 | 15 | export default HttpException; 16 | -------------------------------------------------------------------------------- /business-dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Business Dashboard 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /react-frontend/src/config/utils.ts: -------------------------------------------------------------------------------- 1 | export const validEmail = (email: string) => { 2 | let emailRegx = 3 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 4 | return emailRegx.test(email); 5 | }; 6 | 7 | export const validPhone = (phone: string) => { 8 | const phoneRegex = /^[0-9]{10}$/; 9 | return phoneRegex.test(phone); 10 | }; 11 | -------------------------------------------------------------------------------- /backend/src/exceptions/UnprocessableEntityException.ts: -------------------------------------------------------------------------------- 1 | import { statusCodes } from "../config/const.config"; 2 | import HttpException from "./HttpException"; 3 | 4 | class UnprocessableEntityException extends HttpException { 5 | constructor(msg?: string, code?: string) { 6 | super( 7 | statusCodes.UNPROCESSABLE_ENTITY, 8 | msg ?? "Unprocessable entity", 9 | code, 10 | ); 11 | } 12 | } 13 | 14 | export default UnprocessableEntityException; 15 | -------------------------------------------------------------------------------- /react-frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:react-hooks/recommended", 7 | ], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | plugins: ["react-refresh"], 11 | rules: { 12 | "react-refresh/only-export-components": "warn", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/exceptions/UnauthorizedException.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "./HttpException"; 2 | import { errorCodes, statusCodes } from "../config/const.config"; 3 | 4 | class UnauthorizedException extends HttpException { 5 | constructor(msg?: string, code?: string) { 6 | super( 7 | statusCodes.UNAUTHORIZED, 8 | msg ?? "User is not authorized", 9 | errorCodes.UNAUTHORIZED_ERROR, 10 | ); 11 | } 12 | } 13 | 14 | export default UnauthorizedException; 15 | -------------------------------------------------------------------------------- /business-dashboard/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:react-hooks/recommended", 7 | ], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | plugins: ["react-refresh"], 11 | rules: { 12 | "react-refresh/only-export-components": "warn", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /business-dashboard/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply font-normal text-base text-body bg-whiten relative z-1; 8 | } 9 | } 10 | 11 | @layer utilities { 12 | /* Chrome, Safari and Opera */ 13 | .no-scrollbar::-webkit-scrollbar { 14 | display: none; 15 | } 16 | 17 | .no-scrollbar { 18 | -ms-overflow-style: none; /* IE and Edge */ 19 | scrollbar-width: none; /* Firefox */ 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /business-dashboard/src/store/categories.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const categorySlice = createSlice({ 4 | name: "category", 5 | initialState: { 6 | categories: [], 7 | }, 8 | reducers: { 9 | setCategories: (state, action) => { 10 | state.categories = action.payload; 11 | }, 12 | }, 13 | }); 14 | 15 | // Action creators are generated for each case reducer function 16 | export const { setCategories } = categorySlice.actions; 17 | 18 | export default categorySlice.reducer; 19 | -------------------------------------------------------------------------------- /react-admin/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply font-normal text-base text-body bg-whiten relative z-1; 8 | } 9 | } 10 | 11 | @layer utilities { 12 | /* Chrome, Safari and Opera */ 13 | .no-scrollbar::-webkit-scrollbar { 14 | display: none; 15 | } 16 | 17 | .no-scrollbar { 18 | -ms-overflow-style: none; /* IE and Edge */ 19 | scrollbar-width: none; /* Firefox */ 20 | } 21 | } 22 | 23 | [x-cloak] { 24 | display: none !important; 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/business_type/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query } from "type-graphql"; 2 | import { handleErrors } from "../util/handlers.util"; 3 | import BusinessType from "./model"; 4 | 5 | @Resolver(() => BusinessType) 6 | export class BusinessTypeResolver { 7 | @Query(() => [BusinessType]) 8 | async businessTypes(): Promise<[BusinessType]> { 9 | let businessTypes: any; 10 | try { 11 | businessTypes = await BusinessType.findAll(); 12 | } catch (error: any) { 13 | handleErrors(error); 14 | } 15 | 16 | return businessTypes; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /react-frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import App from "./App"; 3 | import "./assets/css/index.css"; 4 | import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client"; 5 | 6 | const client = new ApolloClient({ 7 | uri: import.meta.env.VITE_GRAPHQL_SERVER_URL, 8 | cache: new InMemoryCache(), 9 | name: "React frontend", 10 | version: "1.0", 11 | }); 12 | 13 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 14 | 15 | 16 | , 17 | ); 18 | -------------------------------------------------------------------------------- /react-admin/src/hooks/useColorMode.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import useLocalStorage from "./useLocalStorage"; 3 | 4 | const useColorMode = () => { 5 | const [colorMode, setColorMode] = useLocalStorage("color-theme", "light"); 6 | 7 | useEffect(() => { 8 | const className = "dark"; 9 | const bodyClass = window.document.body.classList; 10 | 11 | colorMode === "dark" 12 | ? bodyClass.add(className) 13 | : bodyClass.remove(className); 14 | }, [colorMode]); 15 | 16 | return [colorMode, setColorMode]; 17 | }; 18 | 19 | export default useColorMode; 20 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["src"], 4 | "outDir": "./dist", 5 | "lib": ["es2020", "esnext.asynciterable"], 6 | "target": "es6", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "types": ["node", "@types/jest"], 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "strictPropertyInitialization": false 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /react-admin/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import App from "./App"; 4 | import "./assets/css/index.css"; 5 | import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client"; 6 | 7 | const client = new ApolloClient({ 8 | uri: import.meta.env.VITE_GRAPHQL_SERVER_URL, 9 | cache: new InMemoryCache(), 10 | name: "React admin panel", 11 | version: "1.0", 12 | }); 13 | 14 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 15 | 16 | 17 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /backend/src/business_type/model.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from "sequelize"; 2 | import sequelize from "../config/db.config"; 3 | import { Field, ObjectType } from "type-graphql"; 4 | 5 | @ObjectType() 6 | class BusinessType extends Model { 7 | @Field() 8 | public id!: number; 9 | @Field() 10 | public type!: string; 11 | } 12 | 13 | BusinessType.init( 14 | { 15 | id: { 16 | type: DataTypes.INTEGER, 17 | primaryKey: true, 18 | autoIncrement: true, 19 | }, 20 | type: { 21 | type: DataTypes.STRING, 22 | allowNull: false, 23 | }, 24 | }, 25 | { 26 | sequelize, 27 | modelName: "business_type", 28 | }, 29 | ); 30 | 31 | export default BusinessType; 32 | -------------------------------------------------------------------------------- /react-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | "composite": true, 23 | "allowSyntheticDefaultImports": true 24 | }, 25 | "include": ["src", "vite.config.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/config/const.config.ts: -------------------------------------------------------------------------------- 1 | const roles = { 2 | ADMIN: 1, 3 | OWNER: 2, 4 | USER: 3, 5 | }; 6 | 7 | const statusCodes = { 8 | OK: 200, 9 | CREATED: 201, 10 | BAD_REQUEST: 400, 11 | UNAUTHORIZED: 401, 12 | FORBIDDEN: 403, 13 | NOT_FOUND: 404, 14 | NOT_ACCEPTABLE: 406, 15 | UNPROCESSABLE_ENTITY: 422, 16 | SERVER_ERROR: 500, 17 | }; 18 | 19 | const errorCodes = { 20 | UNIQUE_CONSTRAINT_ERROR: "UNIQUE_CONSTRAINT_ERROR", 21 | DATABASE_ERROR: "DATABASE_ERROR", 22 | UNAUTHORIZED_ERROR: "UNAUTHORIZED_ERROR", 23 | DATABASE_VALIDATION_ERROR: "DATABASE_VALIDATION_ERROR", 24 | }; 25 | 26 | const businessTypes = { 27 | RESTAURANT: 1, 28 | }; 29 | 30 | export { roles, statusCodes, businessTypes, errorCodes }; 31 | -------------------------------------------------------------------------------- /react-admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Omnidashboard Admin 8 | 9 | 10 |
14 |
17 |
18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /react-admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | "types": ["vite/client"], 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "composite": true, 21 | "allowSyntheticDefaultImports": true 22 | }, 23 | "include": ["src", "src/lib.d.ts", "vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /react-admin/src/components/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | interface BreadcrumbProps { 3 | pageName: string; 4 | } 5 | const Breadcrumb = ({ pageName }: BreadcrumbProps) => { 6 | return ( 7 |
8 |

9 | {pageName} 10 |

11 | 12 | 20 |
21 | ); 22 | }; 23 | 24 | export default Breadcrumb; 25 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | interface BreadcrumbProps { 3 | pageName: string; 4 | } 5 | const Breadcrumb = ({ pageName }: BreadcrumbProps) => { 6 | return ( 7 |
8 |

9 | {pageName} 10 |

11 | 12 | 20 |
21 | ); 22 | }; 23 | 24 | export default Breadcrumb; 25 | -------------------------------------------------------------------------------- /business-dashboard/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import storage from "redux-persist/lib/storage"; 3 | import { persistReducer, persistStore } from "redux-persist"; 4 | import thunk from "redux-thunk"; 5 | import categoryReducer from "./categories"; 6 | 7 | const persistConfig = { 8 | key: "root", 9 | storage, 10 | }; 11 | 12 | const reducers = combineReducers({ 13 | category: categoryReducer, 14 | }); 15 | 16 | const persistedReducer = persistReducer(persistConfig, reducers); 17 | 18 | export const store = configureStore({ 19 | reducer: persistedReducer, 20 | devTools: process.env.NODE_ENV !== "production", 21 | middleware: [thunk], 22 | }); 23 | 24 | export const persistor = persistStore(store); 25 | -------------------------------------------------------------------------------- /react-admin/src/graphQL/queries.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | const GET_USERS = gql` 4 | query Users { 5 | users { 6 | id 7 | name 8 | email 9 | city 10 | username 11 | password 12 | business { 13 | name 14 | status 15 | link_id 16 | } 17 | } 18 | } 19 | `; 20 | 21 | const GET_USER = gql` 22 | query FindUser($id: String!) { 23 | user(id: $id) { 24 | id 25 | name 26 | email 27 | phone 28 | city 29 | role_id 30 | gender 31 | username 32 | password 33 | business { 34 | name 35 | description 36 | address 37 | } 38 | } 39 | } 40 | `; 41 | 42 | export { GET_USERS, GET_USER }; 43 | -------------------------------------------------------------------------------- /backend/src/business_type/api-doc.md: -------------------------------------------------------------------------------- 1 | # API list 2 | 3 | ## 1. Get business types 4 | 5 |
6 | Get all business types 7 |
8 | 9 | ## Get users 10 | 11 | - **Description** : This API is used to get all business type. 12 | - **Request type** : query 13 | - **Request body sample**: 14 | 15 | - **Request body** 16 | 17 | ``` 18 | query businessTypes() { 19 | businessTypes { 20 | id 21 | type 22 | } 23 | } 24 | ``` 25 | 26 | - **Response**: 27 | 28 | ```json 29 | { 30 | "data": { 31 | "businessTypes": [ 32 | { 33 | "id": 1, 34 | "type": "Type1" 35 | }, 36 | { 37 | "id": 2, 38 | "type": "Type2" 39 | } 40 | ] 41 | } 42 | } 43 | ``` 44 | 45 |
46 | -------------------------------------------------------------------------------- /business-dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | "types": ["vite/client", "vite-plugin-pwa/client"], 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "composite": true, 21 | "allowSyntheticDefaultImports": true 22 | }, 23 | "include": ["src", "src/lib.d.ts", "vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # API references 2 | 3 | ## Common Error Codes 4 | 5 | - **400** : Bad request - In case any required data is missing in request 6 | - **401** : Unauthorized - If request is unauthorized 7 | - **404** : Not found - If requested entity is not available 8 | - **500** : Internal server error - Any unexpected error 9 |

10 | 11 | ## API used for business dashboard 12 | 13 | - [Business](https://github.com/canopas/omniDashboard/blob/main/backend/src/business/api-doc.md) 14 | - [Business Types](https://github.com/canopas/omniDashboard/blob/main/backend/src/business_type/api-doc.md) 15 | - [Business Users](https://github.com/canopas/omniDashboard/blob/main/backend/src/business_user/api-doc.md) 16 | - [Categories](https://github.com/canopas/omniDashboard/blob/main/backend/src/category/api-doc.md) 17 | 18 | ## API used for admin panel 19 | 20 | - [Users](https://github.com/canopas/omniDashboard/blob/main/backend/src/user/api-doc.md) 21 | - [Admin](https://github.com/canopas/omniDashboard/blob/main/backend/src/admin/api-doc.md) 22 | -------------------------------------------------------------------------------- /react-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@apollo/client": "^3.7.14", 14 | "@headlessui/react": "^1.7.14", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.28", 20 | "@types/react-dom": "^18.0.11", 21 | "@typescript-eslint/eslint-plugin": "^5.57.1", 22 | "@typescript-eslint/parser": "^5.57.1", 23 | "@vitejs/plugin-react": "^4.0.0", 24 | "autoprefixer": "^10.4.14", 25 | "eslint": "^8.38.0", 26 | "eslint-plugin-react-hooks": "^4.6.0", 27 | "eslint-plugin-react-refresh": "^0.3.4", 28 | "postcss": "^8.4.23", 29 | "tailwindcss": "^3.3.2", 30 | "typescript": "^5.0.2", 31 | "vite": "^4.3.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Canopas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/category/model.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from "sequelize"; 2 | import sequelize from "../config/db.config"; 3 | import { Field, InputType, ObjectType } from "type-graphql"; 4 | 5 | @ObjectType() 6 | class Category extends Model { 7 | @Field() 8 | public id!: number; 9 | @Field() 10 | public name!: string; 11 | @Field({ nullable: true }) 12 | public parent_id!: number; 13 | @Field() 14 | public business_id?: string; 15 | } 16 | 17 | @InputType() 18 | export class CategoryInput { 19 | @Field() 20 | name!: string; 21 | @Field() 22 | parent_id: number; 23 | @Field() 24 | business_id!: string; 25 | } 26 | 27 | Category.init( 28 | { 29 | id: { 30 | type: DataTypes.INTEGER, 31 | primaryKey: true, 32 | autoIncrement: true, 33 | }, 34 | name: { 35 | type: DataTypes.STRING, 36 | allowNull: false, 37 | }, 38 | parent_id: { 39 | type: DataTypes.INTEGER, 40 | }, 41 | business_id: { 42 | type: DataTypes.STRING, 43 | allowNull: true, 44 | }, 45 | }, 46 | { 47 | sequelize, 48 | modelName: "categories", 49 | }, 50 | ); 51 | 52 | export default Category; 53 | -------------------------------------------------------------------------------- /react-admin/src/components/Alerts/Success.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface ModalProps { 4 | visible: boolean; 5 | title: string; 6 | content: string; 7 | } 8 | 9 | const SuccessAlert = ({ visible, title, content }: ModalProps) => { 10 | const [open, setOpen] = useState(visible); 11 | 12 | setTimeout(() => { 13 | setOpen(false); 14 | }, 3000); 15 | 16 | return ( 17 | <> 18 | {!open ? ( 19 | "" 20 | ) : ( 21 |
22 |
23 | {/* */} 24 |
25 |
26 |
{title}
27 |
    28 |
  • {content}
  • 29 |
30 |
31 |
32 |
33 |
34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default SuccessAlert; 40 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Alerts/Success.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface ModalProps { 4 | visible: boolean; 5 | title: string; 6 | content: string; 7 | } 8 | 9 | const SuccessAlert = ({ visible, title, content }: ModalProps) => { 10 | const [open, setOpen] = useState(visible); 11 | 12 | setTimeout(() => { 13 | setOpen(false); 14 | }, 3000); 15 | 16 | return ( 17 | <> 18 | {!open ? ( 19 | "" 20 | ) : ( 21 |
22 |
23 | {/* */} 24 |
25 |
26 |
{title}
27 |
    28 |
  • {content}
  • 29 |
30 |
31 |
32 |
33 |
34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default SuccessAlert; 40 | -------------------------------------------------------------------------------- /react-admin/src/graphQL/mutations.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | const REGISTRATION = gql` 4 | mutation CreateAdmin($data: AdminInput!) { 5 | createAdmin(data: $data) { 6 | name 7 | email 8 | } 9 | } 10 | `; 11 | 12 | const LOGIN = gql` 13 | mutation AdminLogin($data: AdminInput!) { 14 | adminLogin(data: $data) { 15 | name 16 | email 17 | role_id 18 | } 19 | } 20 | `; 21 | 22 | const DELETE_USER = gql` 23 | mutation DeleteUser($id: Float!) { 24 | deleteUser(id: $id) 25 | } 26 | `; 27 | 28 | const UPDATE_USER = gql` 29 | mutation UpdateUser($id: String!, $data: UserInput!) { 30 | updateUser(id: $id, data: $data) { 31 | id 32 | name 33 | email 34 | phone 35 | city 36 | role_id 37 | gender 38 | username 39 | password 40 | business { 41 | name 42 | description 43 | address 44 | } 45 | } 46 | } 47 | `; 48 | 49 | const SET_BUSINESS_DETAILS = gql` 50 | mutation setBusinessDetails($businessId: String!) { 51 | setBusinessDetails(businessId: $businessId) 52 | } 53 | `; 54 | 55 | export { REGISTRATION, LOGIN, DELETE_USER, UPDATE_USER, SET_BUSINESS_DETAILS }; 56 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnidashboard_node", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "clean": "rm -rf dist", 8 | "build": "yarn clean && tsc -p .", 9 | "start": "yarn build && node dist/index", 10 | "clear-test-cache": "jest --clearCache", 11 | "test": "yarn clear-test-cache && jest --coverage -i --forceExit" 12 | }, 13 | "dependencies": { 14 | "@apollo/server": "^4.7.5", 15 | "@types/bcryptjs": "^2.4.2", 16 | "@types/cors": "^2.8.13", 17 | "@types/express": "^4.17.17", 18 | "@types/jest": "^29.5.1", 19 | "@types/node": "^20.1.2", 20 | "bcryptjs": "^2.4.3", 21 | "class-validator": "^0.14.0", 22 | "cors": "^2.8.5", 23 | "dotenv": "^16.3.1", 24 | "express": "^4.18.2", 25 | "express-graphql": "^0.12.0", 26 | "graphql": "^16.7.1", 27 | "mysql2": "^3.4.2", 28 | "nodemon": "^2.0.22", 29 | "reflect-metadata": "^0.1.13", 30 | "sequelize": "^6.32.1", 31 | "sequelize-typescript": "^2.1.5", 32 | "ts-jest": "^29.1.0", 33 | "ts-node": "^10.9.1", 34 | "type-graphql": "^2.0.0-beta.2", 35 | "typescript": "^5.1.6" 36 | }, 37 | "devDependencies": { 38 | "jest": "^29.5.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/config/db.config.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize-typescript"; 2 | import dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | const hostname = process.env.DB_HOST; 6 | const username = process.env.DB_USERNAME; 7 | const password = process.env.DB_PASSWORD; 8 | const database = process.env.DATABASE_NAME; 9 | const dialect = "mysql"; 10 | 11 | const sequelize = new Sequelize(database!, username!, password, { 12 | host: hostname, 13 | dialect, 14 | repositoryMode: true, 15 | pool: { 16 | max: 10, 17 | min: 0, 18 | acquire: 20000, 19 | idle: 5000, 20 | }, 21 | define: { 22 | freezeTableName: true, 23 | underscored: true, 24 | }, 25 | timezone: tzOffset(), // get client's timeZone, format: +5:30 26 | }); 27 | 28 | function tzOffset() { 29 | // Get the current timezone offset in minutes 30 | const tzOffset = -new Date().getTimezoneOffset(); 31 | 32 | // Convert the offset to the desired format (+HH:MM or -HH:MM) 33 | const hours = Math.floor(tzOffset / 60); 34 | const minutes = Math.abs(tzOffset % 60); 35 | return `${hours >= 0 ? "+" : "-"}${Math.abs(hours) 36 | .toString() 37 | .padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; 38 | } 39 | 40 | export default sequelize; 41 | -------------------------------------------------------------------------------- /react-admin/src/config/utils.ts: -------------------------------------------------------------------------------- 1 | import { SES } from "@aws-sdk/client-ses"; 2 | 3 | export const validEmail = (email: string) => { 4 | let emailRegx = 5 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 6 | return emailRegx.test(email); 7 | }; 8 | 9 | export const validPhone = (phone: string) => { 10 | const phoneRegex = /^[0-9]{10}$/; 11 | return phoneRegex.test(phone); 12 | }; 13 | 14 | export const sendSESMail = async ( 15 | emailHtml: string, 16 | data: string, 17 | receiver: string, 18 | ) => { 19 | const client = new SES({ 20 | region: import.meta.env.VITE_AWS_REGION, 21 | credentials: { 22 | accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID, 23 | secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY_ID, 24 | }, 25 | }); 26 | 27 | const params = { 28 | Source: import.meta.env.VITE_APPROVAL_MAIL_SOURCE, 29 | Destination: { 30 | ToAddresses: [receiver], 31 | }, 32 | Message: { 33 | Body: { 34 | Html: { 35 | Charset: "UTF-8", 36 | Data: emailHtml, 37 | }, 38 | }, 39 | Subject: { 40 | Charset: "UTF-8", 41 | Data: data, 42 | }, 43 | }, 44 | }; 45 | 46 | await client.sendEmail(params); 47 | }; 48 | -------------------------------------------------------------------------------- /backend/src/admin/model.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from "sequelize"; 2 | import sequelize from "../config/db.config"; 3 | import { Field, ObjectType, InputType } from "type-graphql"; 4 | 5 | @ObjectType() 6 | class Admin extends Model { 7 | @Field() 8 | public id!: number; 9 | @Field({ nullable: true }) 10 | public name?: string; 11 | @Field() 12 | public email!: string; 13 | @Field({ nullable: true }) 14 | public role_id?: number; 15 | @Field({ nullable: true }) 16 | public password?: string; 17 | } 18 | 19 | @InputType() 20 | export class AdminInput { 21 | @Field({ nullable: true }) 22 | name?: string; 23 | @Field() 24 | email!: string; 25 | @Field({ nullable: true }) 26 | password!: string; 27 | } 28 | 29 | Admin.init( 30 | { 31 | id: { 32 | type: DataTypes.INTEGER, 33 | primaryKey: true, 34 | autoIncrement: true, 35 | }, 36 | name: { 37 | type: DataTypes.STRING, 38 | allowNull: true, 39 | }, 40 | email: { 41 | type: DataTypes.STRING, 42 | allowNull: false, 43 | }, 44 | role_id: { 45 | type: DataTypes.NUMBER, 46 | defaultValue: 3, 47 | }, 48 | password: { 49 | type: DataTypes.STRING, 50 | allowNull: true, 51 | }, 52 | }, 53 | { 54 | sequelize, 55 | modelName: "users", 56 | }, 57 | ); 58 | 59 | export default Admin; 60 | -------------------------------------------------------------------------------- /business-dashboard/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { VitePWA } from "vite-plugin-pwa"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | server: { 8 | port: 5000, 9 | }, 10 | plugins: [ 11 | react(), 12 | VitePWA({ 13 | registerType: "autoUpdate", 14 | workbox: { 15 | globPatterns: ["**/*"], 16 | cleanupOutdatedCaches: true, 17 | skipWaiting: true, 18 | }, 19 | includeAssets: ["**/*"], 20 | manifest: { 21 | theme_color: "#f69435", 22 | background_color: "#f69435", 23 | display: "standalone", 24 | scope: "/", 25 | start_url: "/", 26 | short_name: "MyBusinessStore", 27 | description: "My business store", 28 | name: "MyBusinessStore", 29 | icons: [ 30 | { 31 | src: "favicon.ico", 32 | sizes: "64x64 32x32 24x24 16x16", 33 | type: "image/x-icon", 34 | }, 35 | { 36 | src: "/logo192.png", 37 | sizes: "192x192", 38 | type: "image/png", 39 | }, 40 | { 41 | src: "/logo512.png", 42 | sizes: "512x512", 43 | type: "image/png", 44 | }, 45 | ], 46 | }, 47 | }), 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /react-admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnidashboard-admin", 3 | "private": true, 4 | "version": "1.0.5", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "author": { 12 | "name": "Canopas", 13 | "url": "https://canopas.com" 14 | }, 15 | "dependencies": { 16 | "@apollo/client": "^3.7.14", 17 | "@aws-sdk/client-ses": "^3.359.0", 18 | "@fortawesome/fontawesome": "^1.1.8", 19 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 20 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 21 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 22 | "@fortawesome/react-fontawesome": "^0.2.0", 23 | "@react-email/html": "^0.0.4", 24 | "@react-email/render": "^0.0.7", 25 | "@types/bcryptjs": "^2.4.2", 26 | "bcryptjs": "^2.4.3", 27 | "react": "^18.2.0", 28 | "react-confirm-alert": "^3.0.6", 29 | "react-dom": "^18.2.0", 30 | "react-router-dom": "^6.8.1" 31 | }, 32 | "devDependencies": { 33 | "@types/react": "^18.0.27", 34 | "@types/react-dom": "^18.0.10", 35 | "@vitejs/plugin-react": "^3.1.0", 36 | "autoprefixer": "^10.4.13", 37 | "file-loader": "^6.2.0", 38 | "postcss": "^8.4.21", 39 | "prettier": "^2.8.4", 40 | "prettier-plugin-tailwindcss": "^0.2.2", 41 | "tailwindcss": "^3.2.6", 42 | "vite": "^4.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /business-dashboard/src/graphQL/queries.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | const GET_USERS = gql` 4 | query businessUsers($businessId: String!) { 5 | businessUsers(businessId: $businessId) { 6 | id 7 | name 8 | email 9 | username 10 | } 11 | } 12 | `; 13 | 14 | const GET_USER = gql` 15 | query BusinessUser($id: String!) { 16 | businessUser(id: $id) { 17 | id 18 | name 19 | email 20 | role_id 21 | username 22 | password 23 | } 24 | } 25 | `; 26 | 27 | const GET_BUSINESS_DETAILS_AND_TYPES = gql` 28 | query ($businessId: String!) { 29 | businessDetails(businessId: $businessId) { 30 | id 31 | name 32 | description 33 | address 34 | city 35 | business_type_id 36 | } 37 | businessTypes { 38 | id 39 | type 40 | } 41 | } 42 | `; 43 | 44 | const GET_CATEGORIES = gql` 45 | query Categories($businessId: String!) { 46 | categories(businessId: $businessId) { 47 | id 48 | name 49 | parent_id 50 | } 51 | } 52 | `; 53 | 54 | const GET_CATEGORY = gql` 55 | query Category($id: String!) { 56 | category(id: $id) { 57 | id 58 | name 59 | parent_id 60 | } 61 | } 62 | `; 63 | 64 | export { 65 | GET_USERS, 66 | GET_USER, 67 | GET_BUSINESS_DETAILS_AND_TYPES, 68 | GET_CATEGORIES, 69 | GET_CATEGORY, 70 | }; 71 | -------------------------------------------------------------------------------- /business-dashboard/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import App from "./App"; 4 | import "./assets/css/index.css"; 5 | import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client"; 6 | import PageNotFound from "./components/Error/404"; 7 | import { store, persistor } from "./store"; 8 | import { Provider } from "react-redux"; 9 | import { PersistGate } from "redux-persist/integration/react"; 10 | import { registerSW } from "virtual:pwa-register"; 11 | 12 | const client = new ApolloClient({ 13 | uri: import.meta.env.VITE_GRAPHQL_SERVER_URL, 14 | cache: new InMemoryCache(), 15 | name: "Business dashboard", 16 | version: "1.0", 17 | }); 18 | 19 | let businessId: string = window.location.pathname.split("/")[1]; 20 | localStorage.setItem("businessId", businessId); 21 | 22 | registerSW({ immediate: true }); 23 | 24 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 25 | 26 | 27 | 28 | {businessId && businessId.length > 0 ? ( 29 | 30 | 31 | 32 | ) : ( 33 | 34 | )} 35 | 36 | 37 | , 38 | ); 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Review checklist 2 | 3 | **Note:** Make sure all points should be checked before submiting PR for review, otherwise PR will be cosidered invalid. 4 | 5 | - [ ] Follow guidelines for creating branch name, adding commit messages or PR title 6 | - [ ] Add MR description if applicable 7 | - [ ] Follow guidelines for naming conventions everywhere (i.e files/folders, data structures) 8 | - [ ] Reuse code (if the same code is written twice, make it common and reuse it at both places) 9 | - [ ] Remove unused or commented code if not required 10 | - [ ] Make MR self-approved - review MR as a reviewer and give it self-approval if everything is ok, if not make the required changes 11 | - [ ] In progress MR should be marked as draft 12 | - [ ] Make MR mark as ready before submitting it for review 13 | - [ ] Add API documentation 14 | - [ ] Add unit tests for APIs 15 | - [ ] Use modular architecture (create files and folders module-wise) 16 | - [ ] Add images, videos, or gifs of UI 17 | - [ ] Add page speed score screenshot of UI (It should be > 85 for mobile and > 90 for desktops) 18 | 19 | **Design should be tested on:** 20 | 21 | - [ ] Inspector (all iPhones and android devices like samsung, motorola) 22 | - [ ] Desktop (chrome, firefox, safari) 23 | - [ ] Laptop / Macbook (chrome, firefox, safari) 24 | - [ ] iPhone(safari) 25 | - [ ] Available android devices (chrome, firefox) 26 | - [ ] iPad(safari) 27 | - [ ] Tablet (chrome, firefox) 28 | -------------------------------------------------------------------------------- /backend/src/admin/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Arg } from "type-graphql"; 2 | import Admin, { AdminInput } from "./model"; 3 | import { roles, statusCodes } from "../config/const.config"; 4 | import { handleErrors } from "../util/handlers.util"; 5 | 6 | @Resolver(() => Admin) 7 | export class AdminResolver { 8 | @Mutation(() => Admin) 9 | async createAdmin(@Arg("data") input: AdminInput): Promise { 10 | let admin: any; 11 | try { 12 | // create admin 13 | admin = await Admin.create({ 14 | name: input.name, 15 | email: input.email, 16 | password: input.password, 17 | role_id: roles.ADMIN, 18 | }); 19 | } catch (error: any) { 20 | handleErrors(error); 21 | } 22 | return admin; 23 | } 24 | 25 | @Mutation(() => Admin) 26 | async adminLogin(@Arg("data") input: AdminInput): Promise { 27 | let admin: any = null; 28 | try { 29 | const existingUser = await Admin.findOne({ 30 | where: { email: input.email }, 31 | }); 32 | if (existingUser != null && existingUser.password == input.password) { 33 | admin = existingUser; 34 | } 35 | } catch (error: any) { 36 | handleErrors(error); 37 | } 38 | 39 | if (admin == null) { 40 | handleErrors({ 41 | code: statusCodes.UNAUTHORIZED, 42 | errors: [{ message: "Invalid credentials!!" }], 43 | }); 44 | } 45 | return admin; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /react-admin/src/hooks/useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | type SetValue = T | ((val: T) => T); 4 | 5 | function useLocalStorage( 6 | key: string, 7 | initialValue: T, 8 | ): [T, (value: SetValue) => void] { 9 | // State to store our value 10 | // Pass initial state function to useState so logic is only executed once 11 | const [storedValue, setStoredValue] = useState(() => { 12 | try { 13 | // Get from local storage by key 14 | const item = window.localStorage.getItem(key); 15 | // Parse stored json or if none return initialValue 16 | return item ? JSON.parse(item) : initialValue; 17 | } catch (error) { 18 | // If error also return initialValue 19 | console.log(error); 20 | return initialValue; 21 | } 22 | }); 23 | 24 | // useEffect to update local storage when the state changes 25 | useEffect(() => { 26 | try { 27 | // Allow value to be a function so we have same API as useState 28 | const valueToStore = 29 | typeof storedValue === "function" 30 | ? storedValue(storedValue) 31 | : storedValue; 32 | // Save state 33 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 34 | } catch (error) { 35 | // A more advanced implementation would handle the error case 36 | console.log(error); 37 | } 38 | }, [key, storedValue]); 39 | 40 | return [storedValue, setStoredValue]; 41 | } 42 | 43 | export default useLocalStorage; 44 | -------------------------------------------------------------------------------- /business-dashboard/src/hooks/useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | type SetValue = T | ((val: T) => T); 4 | 5 | function useLocalStorage( 6 | key: string, 7 | initialValue: T, 8 | ): [T, (value: SetValue) => void] { 9 | // State to store our value 10 | // Pass initial state function to useState so logic is only executed once 11 | const [storedValue, setStoredValue] = useState(() => { 12 | try { 13 | // Get from local storage by key 14 | const item = window.localStorage.getItem(key); 15 | // Parse stored json or if none return initialValue 16 | return item ? JSON.parse(item) : initialValue; 17 | } catch (error) { 18 | // If error also return initialValue 19 | console.log(error); 20 | return initialValue; 21 | } 22 | }); 23 | 24 | // useEffect to update local storage when the state changes 25 | useEffect(() => { 26 | try { 27 | // Allow value to be a function so we have same API as useState 28 | const valueToStore = 29 | typeof storedValue === "function" 30 | ? storedValue(storedValue) 31 | : storedValue; 32 | // Save state 33 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 34 | } catch (error) { 35 | // A more advanced implementation would handle the error case 36 | console.log(error); 37 | } 38 | }, [key, storedValue]); 39 | 40 | return [storedValue, setStoredValue]; 41 | } 42 | 43 | export default useLocalStorage; 44 | -------------------------------------------------------------------------------- /react-admin/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import SignIn from "./pages/Authentication/SignIn"; 4 | import SignUp from "./pages/Authentication/SignUp"; 5 | import { PrivateRoute } from "./pages/Authentication/PrivateRoute"; 6 | import Users from "./components/Users/Index"; 7 | import UserEdit from "./components/Users/Edit"; 8 | 9 | function App() { 10 | const [loading, setLoading] = useState(true); 11 | 12 | const preloader = document.getElementById("preloader"); 13 | 14 | if (preloader) { 15 | setTimeout(() => { 16 | preloader.style.display = "none"; 17 | setLoading(false); 18 | }, 2000); 19 | } 20 | 21 | useEffect(() => { 22 | setTimeout(() => setLoading(false), 1000); 23 | }, []); 24 | 25 | return loading ? ( 26 |

Failed to load app

27 | ) : ( 28 | <> 29 | 30 | } /> 31 | } /> 32 | 36 | 37 | 38 | } 39 | /> 40 | 44 | 45 | 46 | } 47 | /> 48 | 49 | 50 | ); 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /react-admin/src/components/DarkModeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import useColorMode from "../hooks/useColorMode"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons"; 4 | 5 | const DarkModeSwitcher = () => { 6 | const [colorMode, setColorMode] = useColorMode(); 7 | 8 | return ( 9 |
  • 10 | 37 |
  • 38 | ); 39 | }; 40 | 41 | export default DarkModeSwitcher; 42 | -------------------------------------------------------------------------------- /backend/src/util/handlers.util.ts: -------------------------------------------------------------------------------- 1 | import { errorCodes, statusCodes } from "../config/const.config"; 2 | import BadRequestException from "../exceptions/BadRequestException"; 3 | import ServerErrorException from "../exceptions/ServerErrorException"; 4 | import UnauthorizedException from "../exceptions/UnauthorizedException"; 5 | 6 | export function handleErrors(error: any) { 7 | const message = error.errors ? error.errors[0].message : undefined; 8 | if (error.code == statusCodes.UNAUTHORIZED) { 9 | throw new UnauthorizedException(message || "User is not authorized."); 10 | } 11 | if (error.name === "SequelizeValidationError") { 12 | throw new BadRequestException( 13 | message || "Validation error occurred.", 14 | errorCodes.DATABASE_VALIDATION_ERROR, 15 | ); 16 | } else if (error.name === "SequelizeUniqueConstraintError") { 17 | throw new BadRequestException( 18 | message || "Unique constraint violation error occurred.", 19 | errorCodes.UNIQUE_CONSTRAINT_ERROR, 20 | ); 21 | } else { 22 | throw new ServerErrorException( 23 | message || "An error occurred at server", 24 | errorCodes.DATABASE_ERROR, 25 | ); 26 | } 27 | } 28 | 29 | export const generateRandomString = (length: number) => { 30 | const chars = 31 | "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890@#$!"; 32 | const randomArray = Array.from( 33 | { length: length }, 34 | (v, k) => chars[Math.floor(Math.random() * chars.length)], 35 | ); 36 | 37 | const randomString = randomArray.join(""); 38 | return randomString; 39 | }; 40 | -------------------------------------------------------------------------------- /react-admin/src/components/Alerts/Error.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface ModalProps { 4 | visible: boolean; 5 | title: string; 6 | content: string; 7 | } 8 | 9 | const ErrorAlert = ({ visible, title, content }: ModalProps) => { 10 | const [open, setOpen] = useState(visible); 11 | 12 | return ( 13 | <> 14 | {!open ? ( 15 | "" 16 | ) : ( 17 |
    18 |
    19 | {/* */} 20 |
    21 |
    22 |
    {title}
    23 |
      24 |
    • {content}
    • 25 |
    26 |
    27 |
    28 | 35 |
    36 |
    37 |
    38 |
    39 | )} 40 | 41 | ); 42 | }; 43 | 44 | export default ErrorAlert; 45 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Alerts/Error.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface ModalProps { 4 | visible: boolean; 5 | title: string; 6 | content: string; 7 | } 8 | 9 | const ErrorAlert = ({ visible, title, content }: ModalProps) => { 10 | const [open, setOpen] = useState(visible); 11 | 12 | return ( 13 | <> 14 | {!open ? ( 15 | "" 16 | ) : ( 17 |
    18 |
    19 | {/* */} 20 |
    21 |
    22 |
    {title}
    23 |
      24 |
    • {content}
    • 25 |
    26 |
    27 |
    28 | 35 |
    36 |
    37 |
    38 |
    39 | )} 40 | 41 | ); 42 | }; 43 | 44 | export default ErrorAlert; 45 | -------------------------------------------------------------------------------- /business-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@apollo/client": "^3.7.16", 14 | "@aws-sdk/client-ses": "^3.363.0", 15 | "@fortawesome/fontawesome": "^1.1.8", 16 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 17 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 18 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 19 | "@fortawesome/react-fontawesome": "^0.2.0", 20 | "@reduxjs/toolkit": "^1.9.5", 21 | "autoprefixer": "^10.4.14", 22 | "bcryptjs": "^2.4.3", 23 | "graphql": "^16.7.1", 24 | "postcss": "^8.4.24", 25 | "react": "^18.2.0", 26 | "react-confirm-alert": "^3.0.6", 27 | "react-dom": "^18.2.0", 28 | "react-redux": "^8.1.1", 29 | "react-router-dom": "^6.14.1", 30 | "redux-persist": "^6.0.0", 31 | "tailwindcss": "^3.3.2" 32 | }, 33 | "devDependencies": { 34 | "@types/bcryptjs": "^2.4.2", 35 | "@types/react": "^18.0.37", 36 | "@types/react-dom": "^18.0.11", 37 | "@typescript-eslint/eslint-plugin": "^5.59.0", 38 | "@typescript-eslint/parser": "^5.59.0", 39 | "@vitejs/plugin-react": "^4.0.0", 40 | "eslint": "^8.38.0", 41 | "eslint-plugin-react-hooks": "^4.6.0", 42 | "eslint-plugin-react-refresh": "^0.3.4", 43 | "typescript": "^5.0.2", 44 | "vite": "^4.3.9", 45 | "vite-plugin-pwa": "^0.16.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /react-admin/src/layout/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | import Header from "../components/Header"; 3 | import Sidebar from "../components/Sidebar/Sidebar"; 4 | 5 | interface DefaultLayoutProps { 6 | children: ReactNode; 7 | } 8 | 9 | const DefaultLayout = ({ children }: DefaultLayoutProps) => { 10 | const [sidebarOpen, setSidebarOpen] = useState(false); 11 | 12 | return ( 13 |
    14 | {/* */} 15 |
    16 | {/* */} 17 | 18 | {/* */} 19 | 20 | {/* */} 21 |
    22 | {/* */} 23 |
    24 | {/* */} 25 | 26 | {/* */} 27 |
    28 |
    29 | {children} 30 |
    31 |
    32 | 33 | {/* */} 34 |
    35 | {/* */} 36 |
    37 | {/* */} 38 |
    39 | ); 40 | }; 41 | 42 | export default DefaultLayout; 43 | -------------------------------------------------------------------------------- /business-dashboard/src/layout/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | import Header from "../components/Header"; 3 | import Sidebar from "../components/Sidebar/Sidebar"; 4 | 5 | interface DefaultLayoutProps { 6 | children: ReactNode; 7 | } 8 | 9 | const DefaultLayout = ({ children }: DefaultLayoutProps) => { 10 | const [sidebarOpen, setSidebarOpen] = useState(false); 11 | 12 | return ( 13 |
    14 | {/* */} 15 |
    16 | {/* */} 17 | 18 | {/* */} 19 | 20 | {/* */} 21 |
    22 | {/* */} 23 |
    24 | {/* */} 25 | 26 | {/* */} 27 |
    28 |
    29 | {children} 30 |
    31 |
    32 | 33 | {/* */} 34 |
    35 | {/* */} 36 |
    37 | {/* */} 38 |
    39 | ); 40 | }; 41 | 42 | export default DefaultLayout; 43 | -------------------------------------------------------------------------------- /backend/src/business/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Arg, Query, Mutation } from "type-graphql"; 2 | import Business, { BusinessInput } from "../business/model"; 3 | import { handleErrors } from "../util/handlers.util"; 4 | import NotFoundException from "../exceptions/NotFoundException"; 5 | 6 | @Resolver(() => Business) 7 | export class BusinessResolver { 8 | @Query(() => Business) 9 | async businessDetails( 10 | @Arg("businessId") businessId: string, 11 | ): Promise { 12 | return this.findBusinessById(businessId); 13 | } 14 | 15 | @Mutation(() => Business) 16 | async updateBusinessDetails( 17 | @Arg("businessId") businessId: string, 18 | @Arg("data") input: BusinessInput, 19 | ): Promise { 20 | try { 21 | // update user 22 | await Business.update( 23 | { 24 | name: input.name, 25 | description: input.description, 26 | address: input.address, 27 | city: input.city, 28 | business_type_id: input.business_type_id, 29 | }, 30 | { 31 | where: { link_id: businessId }, 32 | }, 33 | ); 34 | } catch (error: any) { 35 | handleErrors(error); 36 | } 37 | 38 | return this.findBusinessById(businessId); 39 | } 40 | 41 | async findBusinessById(businessId: string): Promise { 42 | let business: any; 43 | try { 44 | business = await Business.findOne({ 45 | where: { link_id: businessId }, 46 | }); 47 | } catch (error: any) { 48 | handleErrors(error); 49 | } 50 | 51 | if (business == null) { 52 | throw new NotFoundException(); 53 | } 54 | 55 | return business; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/business_type/__tests__/types.test.ts: -------------------------------------------------------------------------------- 1 | import ServerErrorException from "../../exceptions/ServerErrorException"; 2 | import BusinessType from "../model"; 3 | import { BusinessTypeResolver } from "../resolver"; 4 | 5 | describe("TypeResolver", () => { 6 | let typeResolver: BusinessTypeResolver; 7 | 8 | const types: BusinessType[] = [ 9 | { 10 | id: 1, 11 | type: "type1", 12 | }, 13 | { 14 | id: 2, 15 | type: "type2", 16 | }, 17 | ] as BusinessType[]; 18 | 19 | beforeAll(() => { 20 | typeResolver = new BusinessTypeResolver(); 21 | }); 22 | 23 | describe("getBusinessTypes", () => { 24 | it("should handle errors while getting types", async () => { 25 | const findMock = jest 26 | .spyOn(BusinessType, "findAll") 27 | .mockRejectedValueOnce( 28 | new ServerErrorException("An error occurred at server"), 29 | ); 30 | 31 | try { 32 | await typeResolver.businessTypes(); 33 | } catch (error: any) { 34 | expect(error).toBeInstanceOf(ServerErrorException); 35 | expect(error.message).toBe("An error occurred at server"); 36 | expect(findMock).toHaveBeenCalledTimes(1); 37 | } 38 | 39 | findMock.mockRestore(); // Restore the original implementation 40 | }); 41 | 42 | it("should get all business types", async () => { 43 | const findMock = jest 44 | .spyOn(BusinessType, "findAll") 45 | .mockResolvedValueOnce(types); 46 | 47 | const result = await typeResolver.businessTypes(); 48 | 49 | expect(result).toEqual(types); 50 | expect(findMock).toHaveBeenCalledTimes(1); 51 | 52 | findMock.mockRestore(); // Restore the original implementation 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /backend/src/admin/api-doc.md: -------------------------------------------------------------------------------- 1 | # API list 2 | 3 | ## 1. Create admin 4 | 5 |
    6 | Create admin 7 |
    8 | 9 | ## Register 10 | 11 | - **Description** : This API is used to register the admin from admin panel. 12 | - **Request type** : mutation 13 | - **Request body sample**: 14 | 15 | - **Request body** 16 | 17 | ``` 18 | mutation CreateAdmin($data: AdminInput!) { 19 | createAdmin(data: $data) { 20 | name 21 | email 22 | } 23 | } 24 | ``` 25 | 26 | - **Add variables like below** 27 | 28 | ``` 29 | { 30 | "data": { 31 | "name" : "name", 32 | "email": "email", 33 | "password": "encrypted password" 34 | }, 35 | } 36 | ``` 37 | 38 | - **Response**: 39 | 40 | ```json 41 | { 42 | "data": { 43 | "createAdmin": { 44 | "name": "test", 45 | "email": "test@gmail.com" 46 | } 47 | } 48 | } 49 | ``` 50 | 51 |
    52 | 53 | ## 2. Admin login 54 | 55 |
    56 | Admin login 57 |
    58 | 59 | ## Admin login 60 | 61 | - **Description** : This API is used to login the admin from admin panel. 62 | - **Request type** : mutation 63 | - **Request body sample**: 64 | 65 | - **Request body** 66 | 67 | ``` 68 | mutation AdminLogin($data: AdminInput!) { 69 | adminLogin(data: $data) { 70 | name 71 | email 72 | role_id 73 | } 74 | } 75 | ``` 76 | 77 | - **Add variables like below** 78 | 79 | ``` 80 | { 81 | "data": { 82 | "email": "email", 83 | "password": "encrypted password" 84 | }, 85 | } 86 | ``` 87 | 88 | - **Response**: 89 | 90 | ```json 91 | { 92 | "data": { 93 | "adminLogin": { 94 | "name": "test", 95 | "email": "test@gmail.com", 96 | "role_id": 1 97 | } 98 | } 99 | } 100 | ``` 101 | 102 |
    103 | -------------------------------------------------------------------------------- /business-dashboard/src/graphQL/mutations.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | const LOGIN = gql` 4 | mutation Login($data: LoginInput!) { 5 | login(data: $data) { 6 | id 7 | name 8 | email 9 | role_id 10 | } 11 | } 12 | `; 13 | 14 | const CREATE_USER = gql` 15 | mutation CreateBusinessUser($data: BusinessUserInput!) { 16 | createBusinessUser(data: $data) { 17 | id 18 | name 19 | email 20 | username 21 | } 22 | } 23 | `; 24 | 25 | const DELETE_USER = gql` 26 | mutation DeleteBusinessUser($id: Float!) { 27 | deleteBusinessUser(id: $id) 28 | } 29 | `; 30 | 31 | const UPDATE_USER = gql` 32 | mutation UpdateBusinessUser($id: String!, $data: BusinessUserInput!) { 33 | updateBusinessUser(id: $id, data: $data) { 34 | id 35 | name 36 | email 37 | role_id 38 | username 39 | password 40 | } 41 | } 42 | `; 43 | 44 | const UPDATE_BUSINESS_DETAILS = gql` 45 | mutation UpdateBusinessDetails($businessId: String!, $data: BusinessInput!) { 46 | updateBusinessDetails(businessId: $businessId, data: $data) { 47 | id 48 | name 49 | description 50 | address 51 | city 52 | business_type_id 53 | } 54 | } 55 | `; 56 | 57 | const CREATE_CATEGORY = gql` 58 | mutation CreateCategory($data: CategoryInput!) { 59 | createCategory(data: $data) { 60 | id 61 | name 62 | parent_id 63 | } 64 | } 65 | `; 66 | 67 | const UPDATE_CATEGORY = gql` 68 | mutation UpdateCategory($id: String!, $data: CategoryInput!) { 69 | updateCategory(id: $id, data: $data) { 70 | id 71 | name 72 | parent_id 73 | } 74 | } 75 | `; 76 | 77 | const DELETE_CATEGORY = gql` 78 | mutation DeleteCategory($id: Float!) { 79 | deleteCategory(id: $id) 80 | } 81 | `; 82 | 83 | export { 84 | LOGIN, 85 | CREATE_USER, 86 | DELETE_USER, 87 | UPDATE_USER, 88 | UPDATE_BUSINESS_DETAILS, 89 | CREATE_CATEGORY, 90 | UPDATE_CATEGORY, 91 | DELETE_CATEGORY, 92 | }; 93 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ApolloServer } from "@apollo/server"; 3 | import { expressMiddleware } from "@apollo/server/express4"; 4 | import express from "express"; 5 | import { buildSchema } from "type-graphql"; 6 | import { UserResolver } from "./user/resolver"; 7 | import { AdminResolver } from "./admin/resolver"; 8 | import cors from "cors"; 9 | import dotenv from "dotenv"; 10 | import { statusCodes } from "./config/const.config"; 11 | import { BusinessUserResolver } from "./business_user/resolver"; 12 | import { BusinessResolver } from "./business/resolver"; 13 | import { BusinessTypeResolver } from "./business_type/resolver"; 14 | import { CategoryResolver } from "./category/resolver"; 15 | dotenv.config(); 16 | 17 | async function main() { 18 | const schema = await buildSchema({ 19 | resolvers: [ 20 | UserResolver, 21 | AdminResolver, 22 | BusinessUserResolver, 23 | BusinessResolver, 24 | BusinessTypeResolver, 25 | CategoryResolver, 26 | ], 27 | emitSchemaFile: true, 28 | validate: false, 29 | }); 30 | 31 | // apollo server configuration 32 | const server = new ApolloServer({ 33 | schema, 34 | formatError: (error: any) => { 35 | // Access the error code from the extensions field 36 | return { 37 | status: error.extensions.status 38 | ? error.extensions.status 39 | : statusCodes.SERVER_ERROR, 40 | message: error.message, 41 | code: error.extensions.code || "INTERNAL_SERVER_ERROR", 42 | path: error.path, 43 | }; 44 | }, 45 | }); 46 | 47 | await server.start(); 48 | 49 | // express configuration 50 | const app = express(); 51 | app.use( 52 | "/", 53 | cors(), 54 | express.json(), 55 | expressMiddleware(server, { 56 | context: async ({ req }) => ({ token: req.headers.token }), 57 | }), 58 | ); 59 | 60 | await new Promise((resolve) => app.listen({ port: 4000 }, resolve)); 61 | console.log(`Server ready at http://localhost:4000`); 62 | } 63 | 64 | main(); 65 | -------------------------------------------------------------------------------- /backend/src/business_user/model.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from "sequelize"; 2 | import sequelize from "../config/db.config"; 3 | import { Field, ObjectType, InputType } from "type-graphql"; 4 | 5 | @ObjectType() 6 | class BusinessUser extends Model { 7 | @Field() 8 | public id!: number; 9 | @Field({ nullable: true }) 10 | public name?: string; 11 | @Field() 12 | public email!: string; 13 | @Field({ nullable: true }) 14 | public business_id?: string; 15 | @Field({ nullable: true }) 16 | public role_id?: number; 17 | @Field({ nullable: true }) 18 | public username?: string; 19 | @Field({ nullable: true }) 20 | public password?: string; 21 | } 22 | 23 | @InputType() 24 | export class LoginInput { 25 | @Field() 26 | username!: string; 27 | @Field() 28 | businessId!: string; 29 | @Field() 30 | password!: string; 31 | } 32 | 33 | @InputType() 34 | export class BusinessUserInput { 35 | @Field({ nullable: true }) 36 | name?: string; 37 | @Field({ nullable: true }) 38 | email?: string; 39 | @Field({ nullable: true }) 40 | role_id?: number; 41 | @Field({ nullable: true }) 42 | business_id?: string; 43 | @Field({ nullable: true }) 44 | username?: string; 45 | @Field({ nullable: true }) 46 | password?: string; 47 | } 48 | 49 | BusinessUser.init( 50 | { 51 | id: { 52 | type: DataTypes.INTEGER, 53 | primaryKey: true, 54 | autoIncrement: true, 55 | }, 56 | name: { 57 | type: DataTypes.STRING, 58 | allowNull: true, 59 | }, 60 | email: { 61 | type: DataTypes.STRING, 62 | allowNull: false, 63 | }, 64 | role_id: { 65 | type: DataTypes.NUMBER, 66 | defaultValue: 3, 67 | }, 68 | business_id: { 69 | type: DataTypes.STRING, 70 | allowNull: false, 71 | }, 72 | username: { 73 | type: DataTypes.STRING, 74 | allowNull: true, 75 | }, 76 | password: { 77 | type: DataTypes.STRING, 78 | allowNull: true, 79 | }, 80 | }, 81 | { 82 | sequelize, 83 | modelName: "business_users", 84 | }, 85 | ); 86 | 87 | export default BusinessUser; 88 | -------------------------------------------------------------------------------- /backend/src/business/api-doc.md: -------------------------------------------------------------------------------- 1 | # API list 2 | 3 | ## 1. Get business details 4 | 5 |
    6 | Get business details of given id 7 |
    8 | 9 | ## Get business details 10 | 11 | - **Description** : This API is used to get business details of given id. 12 | - **Request type** : query 13 | - **Request body sample**: 14 | 15 | - **Request body** 16 | 17 | ``` 18 | query BusinessDetails($businessId: String!) { 19 | businessDetails(businessId: $businessId) { 20 | id 21 | name 22 | description 23 | address 24 | city 25 | business_type_id 26 | } 27 | } 28 | ``` 29 | 30 | - **Response**: 31 | 32 | ```json 33 | { 34 | "data": { 35 | "businessDetails": { 36 | "id": 1, 37 | "name": "TestBusiness", 38 | "description": "Test business", 39 | "address": "Test address", 40 | "city": "Surat", 41 | "business_type_id": 1 42 | } 43 | } 44 | } 45 | ``` 46 | 47 |
    48 | 49 | ## 2. Update business details 50 | 51 |
    52 | Update business details 53 |
    54 | 55 | ## Update business details 56 | 57 | - **Description** : This API is used to update business details. 58 | - **Request type** : mutation 59 | - **Request body sample**: 60 | 61 | - **Request body** 62 | 63 | ``` 64 | mutation UpdateBusinessUser($id: String!, $data: BusinessUserInput!) { 65 | updateBusinessUser(id: $id, data: $data) { 66 | id 67 | name 68 | email 69 | role_id 70 | username 71 | password 72 | } 73 | } 74 | ``` 75 | 76 | - **Add variables like below** 77 | 78 | ``` 79 | { 80 | "businessId": "business_link_id", 81 | "data": { 82 | "description" : "Test business details" 83 | }, 84 | } 85 | ``` 86 | 87 | - **Response**: 88 | 89 | ```json 90 | { 91 | "data": { 92 | "updateBusinessDetails": { 93 | "id": 1, 94 | "name": "TestBusiness", 95 | "description": "Test business details", 96 | "address": "Test address", 97 | "city": "Surat", 98 | "business_type_id": 1 99 | } 100 | } 101 | } 102 | ``` 103 | 104 |
    105 | -------------------------------------------------------------------------------- /backend/src/business/model.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from "sequelize"; 2 | import sequelize from "../config/db.config"; 3 | import { Field, InputType, ObjectType } from "type-graphql"; 4 | 5 | @ObjectType() 6 | class Business extends Model { 7 | @Field() 8 | public id!: number; 9 | @Field() 10 | public name!: string; 11 | @Field() 12 | public description!: string; 13 | @Field({ nullable: true }) 14 | public address: string; 15 | @Field({ nullable: true }) 16 | public city?: string; 17 | @Field() 18 | public business_type_id!: number; 19 | @Field({ nullable: true }) 20 | public status: number; 21 | @Field({ nullable: true }) 22 | public username: string; 23 | @Field({ nullable: true }) 24 | public password: string; 25 | @Field({ nullable: true }) 26 | public link_id?: string; 27 | } 28 | 29 | @InputType() 30 | export class BusinessInput { 31 | @Field({ nullable: true }) 32 | name?: string; 33 | @Field({ nullable: true }) 34 | description?: string; 35 | @Field({ nullable: true }) 36 | address?: string; 37 | @Field({ nullable: true }) 38 | city?: string; 39 | @Field({ nullable: true }) 40 | business_type_id?: number; 41 | @Field({ nullable: true }) 42 | user_id?: number; 43 | @Field({ nullable: true }) 44 | status?: number; 45 | } 46 | 47 | Business.init( 48 | { 49 | id: { 50 | type: DataTypes.INTEGER, 51 | primaryKey: true, 52 | autoIncrement: true, 53 | }, 54 | name: { 55 | type: DataTypes.STRING, 56 | allowNull: false, 57 | }, 58 | description: { 59 | type: DataTypes.STRING, 60 | allowNull: false, 61 | }, 62 | address: { 63 | type: DataTypes.STRING, 64 | allowNull: false, 65 | }, 66 | business_type_id: { 67 | type: DataTypes.NUMBER, 68 | allowNull: false, 69 | }, 70 | city: { 71 | type: DataTypes.STRING, 72 | allowNull: false, 73 | }, 74 | status: { 75 | type: DataTypes.NUMBER, 76 | defaultValue: 0, 77 | }, 78 | username: { 79 | type: DataTypes.STRING, 80 | allowNull: true, 81 | }, 82 | password: { 83 | type: DataTypes.STRING, 84 | allowNull: true, 85 | }, 86 | link_id: { 87 | type: DataTypes.STRING, 88 | allowNull: true, 89 | }, 90 | }, 91 | { 92 | sequelize, 93 | modelName: "business_details", 94 | }, 95 | ); 96 | 97 | export default Business; 98 | -------------------------------------------------------------------------------- /react-admin/src/components/Email/approval.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Html } from "@react-email/html"; 3 | import { status } from "../../config/const"; 4 | 5 | interface EmailProps { 6 | username: string; 7 | password: string; 8 | mailStatus: number; 9 | businessId: string; 10 | } 11 | 12 | export const Email: React.FC> = ({ 13 | username, 14 | password, 15 | mailStatus, 16 | businessId, 17 | }) => { 18 | return ( 19 | 20 |

    Thank you for registering your business with our application.

    21 |

    22 | We have reviewed your application, and we would like to inform you about 23 | the status of your registration. 24 |

    25 | 26 | {mailStatus == status.APPROVED ? ( 27 | <> 28 |

    29 | Congratulations! We are pleased to inform you that your business 30 | registration has been approved.
    31 | You are now officially a part of our platform. We appreciate your 32 | interest and trust in our application.
    33 |
    34 | You can access your business on
    35 | {import.meta.env.VITE_BUSINESS_DASHBOARD_URL + "/" + businessId} 36 |
    37 | Here are the credentials for you to get started: 38 |

    39 |
      40 |
    • 41 | Username: {username} 42 |
    • 43 |
    • 44 | Password: {password} 45 |
    • 46 |
    47 |

    48 | Note: Please do not share these information with anyone to 49 | prevent threats. 50 |

    {" "} 51 | 52 | ) : ( 53 | <> 54 |

    55 | We regret to inform you that your business registration has been 56 | rejected at this time. We appreciate your interest in joining our 57 | platform, but unfortunately, our requirements are not mathced with 58 | your request. If you believe there has been a mistake or have any 59 | questions, please feel free to reach out to us. 60 |

    61 |

    62 | Thank you for considering our platform, and we wish you the best of 63 | luck with your future endeavors.. 64 |

    65 | 66 | )} 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /backend/src/user/model.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from "sequelize"; 2 | import sequelize from "../config/db.config"; 3 | import { Field, ObjectType, InputType } from "type-graphql"; 4 | import Business, { BusinessInput } from "../business/model"; 5 | 6 | @ObjectType() 7 | class User extends Model { 8 | @Field() 9 | public id!: number; 10 | @Field({ nullable: true }) 11 | public name?: string; 12 | @Field() 13 | public email!: string; 14 | @Field({ nullable: true }) 15 | public phone?: string; 16 | @Field({ nullable: true }) 17 | public city?: string; 18 | @Field({ nullable: true }) 19 | public gender?: number; 20 | @Field({ nullable: true }) 21 | public role_id?: number; 22 | @Field({ nullable: true }) 23 | public username?: string; 24 | @Field({ nullable: true }) 25 | public password?: string; 26 | @Field({ nullable: true }) 27 | public business?: Business; 28 | } 29 | 30 | @InputType() 31 | export class UserInput { 32 | @Field({ nullable: true }) 33 | name?: string; 34 | @Field({ nullable: true }) 35 | email?: string; 36 | @Field({ nullable: true }) 37 | phone?: string; 38 | @Field({ nullable: true }) 39 | city?: string; 40 | @Field({ nullable: true }) 41 | gender?: number; 42 | @Field({ nullable: true }) 43 | role_id?: number; 44 | @Field({ nullable: true }) 45 | username?: string; 46 | @Field({ nullable: true }) 47 | password?: string; 48 | @Field({ nullable: true }) 49 | business?: BusinessInput; 50 | } 51 | 52 | User.init( 53 | { 54 | id: { 55 | type: DataTypes.INTEGER, 56 | primaryKey: true, 57 | autoIncrement: true, 58 | }, 59 | name: { 60 | type: DataTypes.STRING, 61 | allowNull: true, 62 | }, 63 | email: { 64 | type: DataTypes.STRING, 65 | allowNull: false, 66 | }, 67 | phone: { 68 | type: DataTypes.STRING, 69 | allowNull: true, 70 | }, 71 | city: { 72 | type: DataTypes.STRING, 73 | allowNull: true, 74 | }, 75 | gender: { 76 | type: DataTypes.NUMBER, 77 | defaultValue: 0, 78 | }, 79 | role_id: { 80 | type: DataTypes.NUMBER, 81 | defaultValue: 3, 82 | }, 83 | username: { 84 | type: DataTypes.STRING, 85 | allowNull: true, 86 | }, 87 | password: { 88 | type: DataTypes.STRING, 89 | allowNull: true, 90 | }, 91 | }, 92 | { 93 | sequelize, 94 | modelName: "users", 95 | }, 96 | ); 97 | 98 | User.hasOne(Business, { foreignKey: "user_id", as: "business" }); 99 | 100 | export default User; 101 | -------------------------------------------------------------------------------- /business-dashboard/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import SignIn from "./pages/Authentication/SignIn"; 4 | import { PrivateRoute } from "./pages/Authentication/PrivateRoute"; 5 | import Users from "./components/Users"; 6 | import UserEdit from "./components/Users/Edit"; 7 | import UserCreate from "./components/Users/Create"; 8 | import Categories from "./components/Categories"; 9 | import CategoryEdit from "./components/Categories/Edit"; 10 | import CategoryCreate from "./components/Categories/Create"; 11 | import BusinessDetail from "./components/Business"; 12 | 13 | function App() { 14 | const [loading, setLoading] = useState(true); 15 | 16 | const preloader = document.getElementById("preloader"); 17 | 18 | if (preloader) { 19 | setTimeout(() => { 20 | preloader.style.display = "none"; 21 | setLoading(false); 22 | }, 2000); 23 | } 24 | 25 | useEffect(() => { 26 | setTimeout(() => setLoading(false), 1000); 27 | }, []); 28 | 29 | return !loading ? ( 30 | <> 31 | 32 | } /> 33 | 37 | 38 | 39 | } 40 | /> 41 | 45 | 46 | 47 | } 48 | /> 49 | 53 | 54 | 55 | } 56 | /> 57 | 61 | 62 | 63 | } 64 | /> 65 | 69 | 70 | 71 | } 72 | /> 73 | 77 | 78 | 79 | } 80 | /> 81 | 85 | 86 | 87 | } 88 | /> 89 | 90 | 91 | ) : ( 92 | "" 93 | ); 94 | } 95 | 96 | export default App; 97 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import DropdownUser from "./Dropdown/DropdownUser"; 2 | 3 | const Header = (props: { 4 | sidebarOpen: string | boolean | undefined; 5 | setSidebarOpen: (arg0: boolean) => void; 6 | }) => { 7 | return ( 8 |
    9 |
    10 |
    11 | {/* */} 12 | 52 | {/* */} 53 |
    54 | 55 |
    56 | {/* */} 57 | 58 | {/* */} 59 |
    60 |
    61 |
    62 | ); 63 | }; 64 | 65 | export default Header; 66 | -------------------------------------------------------------------------------- /react-frontend/src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useRef, useState } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | 4 | interface ModalProps { 5 | visible: boolean; 6 | title: string; 7 | content: string; 8 | } 9 | 10 | export default function Modal({ visible, title, content }: ModalProps) { 11 | const [open, setOpen] = useState(visible); 12 | 13 | const cancelButtonRef = useRef(null); 14 | 15 | return ( 16 | 17 | 23 | 32 |
    33 | 34 | 35 |
    36 |
    37 | 46 | 47 |
    48 |
    49 |
    50 | 54 | {title} 55 | 56 |
    57 |

    {content}

    58 |
    59 |
    60 |
    61 |
    62 |
    63 | 70 |
    71 |
    72 |
    73 |
    74 |
    75 |
    76 |
    77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /react-admin/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import DarkModeSwitcher from "./DarkModeSwitcher"; 2 | import DropdownUser from "./Dropdown/DropdownUser"; 3 | 4 | const Header = (props: { 5 | sidebarOpen: string | boolean | undefined; 6 | setSidebarOpen: (arg0: boolean) => void; 7 | }) => { 8 | return ( 9 |
    10 |
    11 |
    12 | {/* */} 13 | 53 | {/* */} 54 |
    55 | 56 |
    57 |
      58 | {/* */} 59 | 60 | {/* */} 61 |
    62 | 63 | {/* */} 64 | 65 | {/* */} 66 |
    67 |
    68 |
    69 | ); 70 | }; 71 | 72 | export default Header; 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    Boilerplate with GraphQL, ReactJS, NodeJS, and MySQL

    2 | 3 | This boilerplate project provides a solid foundation for developing web applications using GraphQL, ReactJS, NodeJS, and MySQL. With its intuitive architecture and powerful technologies, this project template empowers developers to quickly build scalable and efficient applications. 4 | 5 | This includes essential configurations, common components, and best practices to help you kickstart your development process. Whether you're a beginner or an experienced developer, this project template is designed to accelerate your workflow and enable you to focus on building innovative features. 6 | 7 | ## Project structure 8 | 9 | The project has 4 sub-projects to handle all the functionalities of the project. 10 | 11 | - [Backend](https://github.com/canopas/omniDashboard/tree/main/backend) - Contains GraphQL APIs 12 | - [React Admin](https://github.com/canopas/omniDashboard/tree/main/react-admin) - Admin panel to manage data 13 | - [React Frontend](https://github.com/canopas/omniDashboard/tree/main/react-frontend) - Frontend to register the business 14 | - [Business dashboard - Progressive Web App (PWA)](https://github.com/canopas/omniDashboard/tree/main/business-dashboard) - Dashboard(Space) for individual business 15 | 16 | ## Requirements 17 | 18 | - Node v20.3.1 19 | - Typescript 20 | 21 | ## Install dependencies 22 | 23 | - Install dependencies 24 | 25 | ``` 26 | yarn install 27 | ``` 28 | 29 | - Add new dependency 30 | 31 | ``` 32 | yarn add 33 | ``` 34 | 35 | ## Run the server 36 | 37 | ### Node Backend 38 | 39 | - Start Node Server 40 | 41 | ``` 42 | yarn start 43 | ``` 44 | 45 | - Run test 46 | 47 | ``` 48 | yarn test 49 | ``` 50 | 51 | You should find GraphQL API running at `http://localhost:4000/graphql`. 52 | 53 | ### React Frontend, React admin and Business dashboard 54 | 55 | - go to the choosen directory using below command, 56 | 57 | ``` 58 | cd 59 | ``` 60 | 61 | - Build project 62 | 63 | ``` 64 | yarn build 65 | ``` 66 | 67 | - Start development Server 68 | 69 | ``` 70 | yarn dev 71 | ``` 72 | 73 | Server will start at `http://localhost:`. 74 | 75 | ## To enable pre-commit hook 76 | 77 | ``` 78 | git config core.hooksPath .githooks 79 | ``` 80 | 81 | ## API documentation 82 | 83 | - Find full API documentation [here](https://github.com/canopas/omniDashboard/blob/main/backend/README.md). 84 | 85 | ## Dependencies 86 | 87 | - [typescript](https://www.typescriptlang.org/) 88 | 89 | ### Backend 90 | 91 | - [graphql-js](https://github.com/graphql/graphql-js) 92 | - [@apollo/server](https://www.apollographql.com/docs/apollo-server/) 93 | - [express](https://expressjs.com/) 94 | - [sequelize](https://sequelize.org/docs/v6/getting-started/) 95 | 96 | ### Frontend 97 | 98 | - [react](https://react.dev/learn) 99 | - [react-redux](https://react-redux.js.org/) 100 | - [@headlessui/react](https://headlessui.com/) 101 | - [@apollo/client](https://www.apollographql.com/docs/react/) 102 | - [graphql-js](https://github.com/graphql/graphql-js) 103 | - [vite](https://vitejs.dev/guide/) 104 | - [tailwindcss](https://tailwindcss.com/docs/guides/create-react-app) 105 | - [font-awesome](https://fontawesome.com/v5/docs/web/use-with/react) 106 | 107 | ### Test 108 | 109 | - [jest](https://github.com/jestjs/jest) 110 | 111 | ## LICENSE 112 | 113 | This repository is released under the [MIT](https://github.com/canopas/omnidashboard/blob/main/LICENSE.md). 114 | -------------------------------------------------------------------------------- /backend/src/category/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Mutation, Query, Resolver } from "type-graphql"; 2 | import Category, { CategoryInput } from "./model"; 3 | import { handleErrors } from "../util/handlers.util"; 4 | import BadRequestException from "../exceptions/BadRequestException"; 5 | import { ApolloServerErrorCode } from "@apollo/server/errors"; 6 | import NotFoundException from "../exceptions/NotFoundException"; 7 | 8 | @Resolver(() => Category) 9 | export class CategoryResolver { 10 | @Query(() => [Category]) 11 | async categories(@Arg("businessId") businessId: string): Promise<[Category]> { 12 | let categories: any; 13 | try { 14 | categories = await Category.findAll({ 15 | where: { business_id: businessId }, 16 | }); 17 | } catch (error: any) { 18 | handleErrors(error); 19 | } 20 | return categories; 21 | } 22 | 23 | @Query(() => Category) 24 | async category(@Arg("id") id: string): Promise { 25 | return this.findByID(id); 26 | } 27 | 28 | @Mutation(() => Category) 29 | async createCategory(@Arg("data") input: CategoryInput): Promise { 30 | if (!input.name || input.business_id == "") { 31 | throw new BadRequestException( 32 | "Name and business id are required!", 33 | ApolloServerErrorCode.BAD_REQUEST, 34 | ); 35 | } 36 | 37 | let category: any; 38 | try { 39 | category = await Category.create({ 40 | name: input.name, 41 | parent_id: input.parent_id, 42 | business_id: input.business_id, 43 | }); 44 | } catch (error: any) { 45 | handleErrors(error); 46 | } 47 | 48 | return category; 49 | } 50 | 51 | @Mutation(() => Category) 52 | async updateCategory( 53 | @Arg("id") id: string, 54 | @Arg("data") input: CategoryInput, 55 | ): Promise { 56 | try { 57 | await Category.update( 58 | { 59 | name: input.name, 60 | parent_id: input.parent_id, 61 | business_id: input.business_id, 62 | }, 63 | { 64 | where: { id: id }, 65 | }, 66 | ); 67 | } catch (error: any) { 68 | handleErrors(error); 69 | } 70 | 71 | return this.findByID(id); 72 | } 73 | 74 | @Mutation(() => Boolean) 75 | async deleteCategory(@Arg("id") id: number): Promise { 76 | let count = 0; 77 | let categories = await this.findByParentID(id.toString()); 78 | 79 | if (categories.length > 0) { 80 | throw new BadRequestException("Please delete it's subcategories first"); 81 | } 82 | 83 | try { 84 | count = await Category.destroy({ where: { id } }); 85 | } catch (error: any) { 86 | handleErrors(error); 87 | } 88 | return count > 0; 89 | } 90 | 91 | async findByID(id: string): Promise { 92 | let category: any; 93 | try { 94 | category = await Category.findOne({ 95 | where: { id: id }, 96 | }); 97 | } catch (error: any) { 98 | handleErrors(error); 99 | } 100 | 101 | if (category == null) { 102 | throw new NotFoundException("Category not found for given Id"); 103 | } 104 | 105 | return category; 106 | } 107 | 108 | async findByParentID(id: string): Promise { 109 | let categories: Category[] = []; 110 | try { 111 | categories = await Category.findAll({ 112 | where: { parent_id: id }, 113 | }); 114 | } catch (error: any) { 115 | handleErrors(error); 116 | } 117 | 118 | return categories; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /react-admin/src/components/Dropdown/DropdownUser.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | import UserOne from "../../assets/images/user/profile.webp"; 4 | import useLocalStorage from "../../hooks/useLocalStorage"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { 7 | faArrowRightFromBracket, 8 | faCaretDown, 9 | } from "@fortawesome/free-solid-svg-icons"; 10 | 11 | const DropdownUser = () => { 12 | const [dropdownOpen, setDropdownOpen] = useState(false); 13 | const [user, _]: any = useLocalStorage("user", ""); 14 | 15 | const trigger = useRef(null); 16 | const dropdown = useRef(null); 17 | 18 | // close on click outside 19 | useEffect(() => { 20 | const clickHandler = ({ target }: MouseEvent) => { 21 | if (!dropdown.current) return; 22 | if ( 23 | !dropdownOpen || 24 | dropdown.current.contains(target) || 25 | trigger.current.contains(target) 26 | ) 27 | return; 28 | setDropdownOpen(false); 29 | }; 30 | document.addEventListener("click", clickHandler); 31 | return () => document.removeEventListener("click", clickHandler); 32 | }); 33 | 34 | // close if the esc key is pressed 35 | useEffect(() => { 36 | const keyHandler = ({ keyCode }: KeyboardEvent) => { 37 | if (!dropdownOpen || keyCode !== 27) return; 38 | setDropdownOpen(false); 39 | }; 40 | document.addEventListener("keydown", keyHandler); 41 | return () => document.removeEventListener("keydown", keyHandler); 42 | }); 43 | 44 | const navigate = useNavigate(); 45 | const logOut = () => { 46 | localStorage.removeItem("user"); 47 | navigate("/signIn"); 48 | }; 49 | 50 | return ( 51 |
    52 | setDropdownOpen(!dropdownOpen)} 55 | className="flex items-center gap-4" 56 | to="#" 57 | > 58 | 59 | 60 | {user.name} 61 | 62 | 63 | {user.role_id == 1 ? "Admin" : "User"} 64 | 65 | 66 | 67 | 68 | User 69 | 70 | 71 | 75 | 76 | 77 | {/* */} 78 |
    setDropdownOpen(true)} 81 | onBlur={() => setDropdownOpen(false)} 82 | className={`absolute right-0 mt-4 flex w-62.5 flex-col rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark ${ 83 | dropdownOpen === true ? "block" : "hidden" 84 | }`} 85 | > 86 | 96 |
    97 | {/* */} 98 |
    99 | ); 100 | }; 101 | 102 | export default DropdownUser; 103 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Dropdown/DropdownUser.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | import UserOne from "../../assets/images/user/profile.webp"; 4 | import useLocalStorage from "../../hooks/useLocalStorage"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { 7 | faArrowRightFromBracket, 8 | faCaretDown, 9 | } from "@fortawesome/free-solid-svg-icons"; 10 | 11 | const DropdownUser = () => { 12 | const [dropdownOpen, setDropdownOpen] = useState(false); 13 | const [user, _]: any = useLocalStorage("user", ""); 14 | 15 | const trigger = useRef(null); 16 | const dropdown = useRef(null); 17 | 18 | // close on click outside 19 | useEffect(() => { 20 | const clickHandler = ({ target }: MouseEvent) => { 21 | if (!dropdown.current) return; 22 | if ( 23 | !dropdownOpen || 24 | dropdown.current.contains(target) || 25 | trigger.current.contains(target) 26 | ) 27 | return; 28 | setDropdownOpen(false); 29 | }; 30 | document.addEventListener("click", clickHandler); 31 | return () => document.removeEventListener("click", clickHandler); 32 | }); 33 | 34 | // close if the esc key is pressed 35 | useEffect(() => { 36 | const keyHandler = ({ keyCode }: KeyboardEvent) => { 37 | if (!dropdownOpen || keyCode !== 27) return; 38 | setDropdownOpen(false); 39 | }; 40 | document.addEventListener("keydown", keyHandler); 41 | return () => document.removeEventListener("keydown", keyHandler); 42 | }); 43 | 44 | const navigate = useNavigate(); 45 | const logOut = () => { 46 | localStorage.removeItem("user"); 47 | navigate("/signIn"); 48 | }; 49 | 50 | return ( 51 |
    52 | setDropdownOpen(!dropdownOpen)} 55 | className="flex items-center gap-4" 56 | to="#" 57 | > 58 | 59 | 60 | {user.name} 61 | 62 | 63 | {user.role_id == 1 ? "Admin" : "User"} 64 | 65 | 66 | 67 | 68 | User 69 | 70 | 71 | 75 | 76 | 77 | {/* */} 78 |
    setDropdownOpen(true)} 81 | onBlur={() => setDropdownOpen(false)} 82 | className={`absolute right-0 mt-4 flex w-62.5 flex-col rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark ${ 83 | dropdownOpen === true ? "block" : "hidden" 84 | }`} 85 | > 86 | 96 |
    97 | {/* */} 98 |
    99 | ); 100 | }; 101 | 102 | export default DropdownUser; 103 | -------------------------------------------------------------------------------- /react-admin/src/components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import { NavLink, useLocation } from "react-router-dom"; 4 | import { faUserGroup } from "@fortawesome/free-solid-svg-icons"; 5 | 6 | interface SidebarProps { 7 | sidebarOpen: boolean; 8 | setSidebarOpen: (arg: boolean) => void; 9 | } 10 | 11 | const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => { 12 | const location = useLocation(); 13 | const { pathname } = location; 14 | 15 | const trigger = useRef(null); 16 | const sidebar = useRef(null); 17 | 18 | const storedSidebarExpanded = localStorage.getItem("sidebar-expanded"); 19 | const [sidebarExpanded] = useState( 20 | storedSidebarExpanded === null ? false : storedSidebarExpanded === "true", 21 | ); 22 | 23 | // close on click outside 24 | useEffect(() => { 25 | const clickHandler = ({ target }: MouseEvent) => { 26 | if (!sidebar.current || !trigger.current) return; 27 | if ( 28 | !sidebarOpen || 29 | sidebar.current.contains(target) || 30 | trigger.current.contains(target) 31 | ) 32 | return; 33 | setSidebarOpen(false); 34 | }; 35 | document.addEventListener("click", clickHandler); 36 | return () => document.removeEventListener("click", clickHandler); 37 | }); 38 | 39 | // close if the esc key is pressed 40 | useEffect(() => { 41 | const keyHandler = ({ keyCode }: KeyboardEvent) => { 42 | if (!sidebarOpen || keyCode !== 27) return; 43 | setSidebarOpen(false); 44 | }; 45 | document.addEventListener("keydown", keyHandler); 46 | return () => document.removeEventListener("keydown", keyHandler); 47 | }); 48 | 49 | useEffect(() => { 50 | localStorage.setItem("sidebar-expanded", sidebarExpanded.toString()); 51 | if (sidebarExpanded) { 52 | document.querySelector("body")?.classList.add("sidebar-expanded"); 53 | } else { 54 | document.querySelector("body")?.classList.remove("sidebar-expanded"); 55 | } 56 | }, [sidebarExpanded]); 57 | 58 | return ( 59 | 98 | ); 99 | }; 100 | 101 | export default Sidebar; 102 | -------------------------------------------------------------------------------- /backend/src/business/__tests__/business.test.ts: -------------------------------------------------------------------------------- 1 | import { BusinessResolver } from "../resolver"; 2 | import Business, { BusinessInput } from "../model"; 3 | import ServerErrorException from "../../exceptions/ServerErrorException"; 4 | import NotFoundException from "../../exceptions/NotFoundException"; 5 | 6 | describe("BusinessResolver", () => { 7 | let businessResolver: BusinessResolver; 8 | const businessID = "er34mgni5o"; 9 | const detail: Business = { 10 | id: 1, 11 | name: "MyBusiness", 12 | } as Business; 13 | 14 | const input: BusinessInput = { 15 | description: "TestDescription", 16 | address: "TestAddress", 17 | city: "Surat", 18 | business_type_id: 1, 19 | }; 20 | 21 | beforeAll(() => { 22 | businessResolver = new BusinessResolver(); 23 | }); 24 | 25 | describe("getBusinessDetails", () => { 26 | it("should handle errors while getting details", async () => { 27 | const findMock = jest 28 | .spyOn(Business, "findOne") 29 | .mockRejectedValueOnce( 30 | new ServerErrorException("An error occurred at server"), 31 | ); 32 | 33 | try { 34 | await businessResolver.businessDetails(businessID); 35 | } catch (error: any) { 36 | expect(error).toBeInstanceOf(ServerErrorException); 37 | expect(error.message).toBe("An error occurred at server"); 38 | expect(findMock).toHaveBeenCalledTimes(1); 39 | } 40 | 41 | findMock.mockRestore(); // Restore the original implementation 42 | }); 43 | 44 | it("should handle not found while getting details", async () => { 45 | const findMock = jest 46 | .spyOn(Business, "findOne") 47 | .mockResolvedValueOnce(null); 48 | 49 | try { 50 | await businessResolver.businessDetails("dtvcxvdgr"); 51 | } catch (error: any) { 52 | expect(error).toBeInstanceOf(NotFoundException); 53 | expect(error.message).toBe("Not found"); 54 | expect(findMock).toHaveBeenCalledTimes(1); 55 | } 56 | 57 | findMock.mockRestore(); // Restore the original implementation 58 | }); 59 | 60 | it("should get given business detail", async () => { 61 | const findMock = jest 62 | .spyOn(Business, "findOne") 63 | .mockResolvedValueOnce(detail); 64 | 65 | const result = await businessResolver.businessDetails(businessID); 66 | 67 | expect(result).toEqual(detail); 68 | expect(findMock).toHaveBeenCalledTimes(1); 69 | 70 | findMock.mockRestore(); // Restore the original implementation 71 | }); 72 | }); 73 | 74 | describe("updateBusinessDetail", () => { 75 | it("should handle errors when updating details", async () => { 76 | const updateMock = jest 77 | .spyOn(Business, "update") 78 | .mockRejectedValueOnce( 79 | new ServerErrorException("An error occurred at server"), 80 | ); 81 | 82 | try { 83 | await businessResolver.updateBusinessDetails("1", input); 84 | } catch (error: any) { 85 | expect(error).toBeInstanceOf(ServerErrorException); 86 | expect(error.message).toBe("An error occurred at server"); 87 | expect(updateMock).toHaveBeenCalledTimes(1); 88 | } 89 | 90 | updateMock.mockRestore(); // Restore the original implementation 91 | }); 92 | 93 | it("should update a business details", async () => { 94 | const updateMock = jest 95 | .spyOn(Business, "update") 96 | .mockResolvedValueOnce([1]); 97 | 98 | const findMock = jest 99 | .spyOn(Business, "findOne") 100 | .mockResolvedValueOnce(detail); 101 | 102 | const result = await businessResolver.updateBusinessDetails("1", input); 103 | 104 | expect(result).toEqual(detail); 105 | 106 | expect(updateMock).toHaveBeenCalledTimes(1); 107 | expect(findMock).toHaveBeenCalledTimes(1); 108 | 109 | updateMock.mockRestore(); // Restore the original implementation 110 | findMock.mockRestore(); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /backend/src/admin/__tests__/admin.test.ts: -------------------------------------------------------------------------------- 1 | import { AdminResolver } from "../resolver"; 2 | import Admin, { AdminInput } from "../model"; 3 | import ServerErrorException from "../../exceptions/ServerErrorException"; 4 | import BadRequestException from "../../exceptions/BadRequestException"; 5 | import UnauthorizedException from "../../exceptions/UnauthorizedException"; 6 | 7 | describe("UserResolver", () => { 8 | let adminResolver: AdminResolver; 9 | 10 | const admin: Admin = { 11 | id: 1, 12 | name: "Admin", 13 | email: "admin@example.com", 14 | } as Admin; 15 | 16 | const input: AdminInput = { 17 | name: "Admin", 18 | email: "admin@example.com", 19 | password: "testAdmin", 20 | }; 21 | 22 | beforeAll(() => { 23 | adminResolver = new AdminResolver(); 24 | }); 25 | 26 | describe("createAdmin", () => { 27 | it("should handle bad request error during admin creation", async () => { 28 | const badInput: AdminInput = {} as AdminInput; 29 | try { 30 | await adminResolver.createAdmin(badInput); 31 | } catch (error: any) { 32 | expect(error).toBeDefined(); 33 | expect(error).toBeInstanceOf(BadRequestException); 34 | expect(error.message).toBe("users.email cannot be null"); 35 | } 36 | }); 37 | 38 | it("should handle server errors during admin creation", async () => { 39 | const createMock = jest 40 | .spyOn(Admin, "create") 41 | .mockRejectedValueOnce( 42 | new ServerErrorException("An error occurred at server"), 43 | ); 44 | 45 | try { 46 | await adminResolver.createAdmin(input); 47 | } catch (error: any) { 48 | expect(error).toBeInstanceOf(ServerErrorException); 49 | expect(error.message).toBe("An error occurred at server"); 50 | expect(createMock).toHaveBeenCalledTimes(1); 51 | } 52 | 53 | createMock.mockRestore(); // Restore the original implementation 54 | }); 55 | 56 | it("should create a user and return the created user", async () => { 57 | const createMock = jest 58 | .spyOn(Admin, "create") 59 | .mockResolvedValueOnce(admin); 60 | 61 | const result = await adminResolver.createAdmin(input); 62 | 63 | expect(result).toEqual(admin); 64 | expect(createMock).toHaveBeenCalledTimes(1); 65 | 66 | createMock.mockRestore(); // Restore the original implementation 67 | }); 68 | }); 69 | 70 | describe("adminLogin", () => { 71 | it("should handle unauthorized error during admin login", async () => { 72 | const findMock = jest.spyOn(Admin, "findOne").mockResolvedValueOnce(null); 73 | try { 74 | input.password = ""; 75 | await adminResolver.adminLogin(input); 76 | } catch (error: any) { 77 | expect(error).toBeInstanceOf(UnauthorizedException); 78 | expect(error.message).toBe("Invalid credentials!!"); 79 | expect(findMock).toHaveBeenCalledTimes(1); 80 | } 81 | 82 | findMock.mockRestore(); // Restore the original implementation 83 | }); 84 | 85 | it("should handle errors during admin login", async () => { 86 | const findMock = jest 87 | .spyOn(Admin, "findOne") 88 | .mockRejectedValueOnce( 89 | new ServerErrorException("An error occurred at server"), 90 | ); 91 | 92 | try { 93 | await adminResolver.adminLogin(input); 94 | } catch (error: any) { 95 | expect(error).toBeInstanceOf(ServerErrorException); 96 | expect(error.message).toBe("An error occurred at server"); 97 | expect(findMock).toHaveBeenCalledTimes(1); 98 | } 99 | 100 | findMock.mockRestore(); // Restore the original implementation 101 | }); 102 | 103 | it("should login as admin", async () => { 104 | admin.password = input.password; 105 | const findMock = jest 106 | .spyOn(Admin, "findOne") 107 | .mockResolvedValueOnce(admin); 108 | 109 | const result = await adminResolver.adminLogin(input); 110 | 111 | expect(result).toEqual(admin); 112 | expect(findMock).toHaveBeenCalledTimes(1); 113 | 114 | findMock.mockRestore(); // Restore the original implementation 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /backend/src/business_user/api-doc.md: -------------------------------------------------------------------------------- 1 | # API list 2 | 3 | ## 1. Set business details 4 | 5 |
    6 | Set business details 7 |
    8 | 9 | ## Register business user 10 | 11 | - **Description** : This API will use to set business details for the perticular business when admin will approve the business. 12 | - **Request type** : mutation 13 | - **Request body sample**: 14 | 15 | - **Request body** 16 | 17 | ``` 18 | mutation setBusinessDetails($businessId: String!) { 19 | setBusinessDetails(businessId: $businessId) 20 | } 21 | ``` 22 | 23 | - **Response**: 24 | 25 | ```json 26 | { 27 | "data": { 28 | "setBusinessDetails": true 29 | } 30 | } 31 | ``` 32 | 33 |
    34 | 35 | ## 2. Get business users 36 | 37 |
    38 | Get all users of the business 39 |
    40 | 41 | ## Get users 42 | 43 | - **Description** : This API is used to get all users of the business. 44 | - **Request type** : query 45 | - **Request body sample**: 46 | 47 | - **Request body** 48 | 49 | ``` 50 | query businessUsers($businessId: String!) { 51 | businessUsers(businessId: $businessId) { 52 | id 53 | name 54 | email 55 | } 56 | } 57 | ``` 58 | 59 | - **Response**: 60 | 61 | ```json 62 | { 63 | "data": { 64 | "users": [ 65 | { 66 | "id": 1, 67 | "name": "User1", 68 | "email": "user1@example.com" 69 | }, 70 | { 71 | "id": 2, 72 | "name": "User2", 73 | "email": "user2@example.com" 74 | } 75 | ] 76 | } 77 | } 78 | ``` 79 | 80 |
    81 | 82 | ## 3. Create user 83 | 84 |
    85 | Create user 86 |
    87 | 88 | ## Create user 89 | 90 | - **Description** : This API is used to create business user. 91 | - **Request type** : mutation 92 | - **Request body sample**: 93 | 94 | - **Request body** 95 | 96 | ``` 97 | mutation CreateBusinessUser($data: BusinessUserInput!) { 98 | createBusinessUser(data: $data) { 99 | id 100 | name 101 | email 102 | username 103 | } 104 | } 105 | ``` 106 | 107 | - **Add variables like below** 108 | 109 | ``` 110 | { 111 | "data": { 112 | "name" : "name", 113 | "email": "email", 114 | "username":"username" 115 | "password": "password" 116 | }, 117 | } 118 | ``` 119 | 120 | - **Response**: 121 | 122 | ```json 123 | { 124 | "data": { 125 | "createBusinessUser": { 126 | "id": 1, 127 | "name": "test", 128 | "email": "test@gmail.com", 129 | "username": "username" 130 | } 131 | } 132 | } 133 | ``` 134 | 135 |
    136 | 137 | ## 4. Update user 138 | 139 |
    140 | Update user 141 |
    142 | 143 | ## Update user 144 | 145 | - **Description** : This API is used to update business user. 146 | - **Request type** : mutation 147 | - **Request body sample**: 148 | 149 | - **Request body** 150 | 151 | ``` 152 | mutation UpdateBusinessUser($id: String!, $data: BusinessUserInput!) { 153 | updateBusinessUser(id: $id, data: $data) { 154 | id 155 | name 156 | email 157 | role_id 158 | username 159 | password 160 | } 161 | } 162 | ``` 163 | 164 | - **Add variables like below** 165 | 166 | ``` 167 | { 168 | "id": "1", 169 | "data": { 170 | "email": "user2@example.com", 171 | }, 172 | } 173 | ``` 174 | 175 | - **Response**: 176 | 177 | ```json 178 | { 179 | "data": { 180 | "updateBusinessUser": { 181 | "name": "User1", 182 | "email": "user2@example.com" 183 | } 184 | } 185 | } 186 | ``` 187 | 188 |
    189 | 190 | ## 5. Delete user 191 | 192 |
    193 | Delete user 194 |
    195 | 196 | ## Delete user 197 | 198 | - **Description** : This API is used to delete business user. 199 | - **Request type** : mutation 200 | - **Request body sample**: 201 | 202 | - **Request body** 203 | 204 | ``` 205 | mutation DeleteBusinessUser($id: Float!) { 206 | deleteBusinessUser(id: $id) 207 | } 208 | ``` 209 | 210 | - **Add variables like below** 211 | 212 | ``` 213 | { 214 | "id": "1", 215 | } 216 | ``` 217 | 218 | - **Response**: 219 | 220 | ```json 221 | { 222 | "data": { 223 | "deleteBusinessUser": true 224 | } 225 | } 226 | ``` 227 | 228 |
    229 | -------------------------------------------------------------------------------- /backend/src/category/api-doc.md: -------------------------------------------------------------------------------- 1 | # API list 2 | 3 | ## 1. Get categories 4 | 5 |
    6 | Get all categories 7 |
    8 | 9 | ## Get categories 10 | 11 | - **Description** : This API is used to get categories. 12 | - **Request type** : query 13 | - **Request body sample**: 14 | 15 | - **Request body** 16 | 17 | ``` 18 | query Categories($businessId: String!) { 19 | categories(businessId: $businessId) { 20 | id 21 | name 22 | parent_id 23 | } 24 | } 25 | ``` 26 | 27 | - **Response**: 28 | 29 | ```json 30 | { 31 | "data": { 32 | "categories": [ 33 | { 34 | "id": 1, 35 | "name": "category1", 36 | "parent_id": 0 37 | }, 38 | { 39 | "id": 2, 40 | "name": "category2", 41 | "parent_id": 1 42 | } 43 | ] 44 | } 45 | } 46 | ``` 47 | 48 |
    49 | 50 | ## 2. Get category by ID 51 | 52 |
    53 | Get category by given id 54 |
    55 | 56 | ## Get category by ID 57 | 58 | - **Description** : This API is used to get category by given id. 59 | - **Request type** : query 60 | - **Request body sample**: 61 | 62 | - **Request body** 63 | 64 | ``` 65 | query Category($id: String!) { 66 | category(id: $id) { 67 | id 68 | name 69 | parent_id 70 | } 71 | } 72 | ``` 73 | 74 | - **Add variable like below** 75 | 76 | ``` 77 | { 78 | "id": "1", 79 | } 80 | ``` 81 | 82 | - **Response**: 83 | 84 | ```json 85 | { 86 | "data": { 87 | "category": { 88 | "id": 1, 89 | "name": "category1", 90 | "parent_id": 0 91 | } 92 | } 93 | } 94 | ``` 95 | 96 |
    97 | 98 | ## 3. Create category 99 | 100 |
    101 | Create category 102 |
    103 | 104 | ## Create category 105 | 106 | - **Description** : This API is used to create category. 107 | - **Request type** : mutation 108 | - **Request body sample**: 109 | 110 | - **Request body** 111 | 112 | ``` 113 | mutation CreateCategory($data: CategoryInput!) { 114 | createCategory(data: $data) { 115 | id 116 | name 117 | parent_id 118 | } 119 | } 120 | ``` 121 | 122 | - **Add variables like below** 123 | 124 | ``` 125 | { 126 | "data": { 127 | "name" : "name", 128 | "parent_id" : "id of parent category or 0", 129 | "business_id": "business id" 130 | }, 131 | } 132 | ``` 133 | 134 | - **Response**: 135 | 136 | ```json 137 | { 138 | "data": { 139 | "createCategory": { 140 | "id": 1, 141 | "name": "categoryName", 142 | "parent_id": 0 143 | } 144 | } 145 | } 146 | ``` 147 | 148 |
    149 | 150 | ## 4. Update category 151 | 152 |
    153 | Update category 154 |
    155 | 156 | ## Update category 157 | 158 | - **Description** : This API is used to update category. 159 | - **Request type** : mutation 160 | - **Request body sample**: 161 | 162 | - **Request body** 163 | 164 | ``` 165 | mutation UpdateCategory($id: String!, $data: CategoryInput!) { 166 | updateCategory(id: $id, data: $data) { 167 | id 168 | name 169 | parent_id 170 | } 171 | } 172 | ``` 173 | 174 | - **Add variables like below** 175 | 176 | ``` 177 | { 178 | "id": "1", 179 | "data": { 180 | "name": "newCategory", 181 | }, 182 | } 183 | ``` 184 | 185 | - **Response**: 186 | 187 | ```json 188 | { 189 | "data": { 190 | "updateCategory": { 191 | "id": 1, 192 | "name": "newCategory", 193 | "parent_id": 0 194 | } 195 | } 196 | } 197 | ``` 198 | 199 |
    200 | 201 | ## 5. Delete category 202 | 203 |
    204 | Delete category 205 |
    206 | 207 | ## Delete category 208 | 209 | - **Description** : This API is used to delete category. 210 | - **Request type** : mutation 211 | - **Request body sample**: 212 | 213 | - **Request body** 214 | 215 | ``` 216 | mutation DeleteCategory($id: Float!) { 217 | deleteCategory(id: $id) 218 | } 219 | ``` 220 | 221 | - **Add variables like below** 222 | 223 | ``` 224 | { 225 | "id": "1", 226 | } 227 | ``` 228 | 229 | - **Response**: 230 | 231 | ```json 232 | { 233 | "data": { 234 | "deleteCategory": true 235 | } 236 | } 237 | ``` 238 | 239 |
    240 | -------------------------------------------------------------------------------- /backend/src/user/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Arg, Query } from "type-graphql"; 2 | import User, { UserInput } from "./model"; 3 | import Business from "../business/model"; 4 | import { roles, businessTypes } from "../config/const.config"; 5 | import { Op } from "sequelize"; 6 | import BadRequestException from "../exceptions/BadRequestException"; 7 | import { ApolloServerErrorCode } from "@apollo/server/errors"; 8 | import { generateRandomString, handleErrors } from "../util/handlers.util"; 9 | import NotFoundException from "../exceptions/NotFoundException"; 10 | 11 | @Resolver(() => User) 12 | export class UserResolver { 13 | @Query(() => [User]) 14 | async users(): Promise<[User]> { 15 | let users: any; 16 | try { 17 | // create user 18 | users = await User.findAll({ 19 | where: { role_id: { [Op.not]: roles.ADMIN } }, 20 | include: { 21 | model: Business, 22 | as: "business", 23 | }, 24 | }); 25 | } catch (error: any) { 26 | handleErrors(error); 27 | } 28 | 29 | return users; 30 | } 31 | 32 | @Query(() => User) 33 | async user(@Arg("id") id: string): Promise { 34 | return this.findUserByID(id); 35 | } 36 | 37 | @Mutation(() => User) 38 | async createUser(@Arg("data") input: UserInput): Promise { 39 | if (!input.email || input.email == "") { 40 | throw new BadRequestException( 41 | "Email is required!", 42 | ApolloServerErrorCode.BAD_REQUEST, 43 | ); 44 | } 45 | 46 | if (!input.business?.name || input.business?.name == "") { 47 | throw new BadRequestException( 48 | "Business details are required!", 49 | ApolloServerErrorCode.BAD_REQUEST, 50 | ); 51 | } 52 | 53 | let user: any; 54 | try { 55 | // create user 56 | user = await User.create({ 57 | name: input.name, 58 | email: input.email, 59 | phone: input.phone, 60 | city: input.city, 61 | role_id: roles.USER, 62 | }); 63 | 64 | if (input.business) { 65 | // create business 66 | await Business.create({ 67 | name: input.business.name, 68 | business_type_id: businessTypes.RESTAURANT, 69 | user_id: user.id, 70 | description: "", 71 | address: "", 72 | link_id: generateRandomString(30), 73 | }); 74 | } 75 | } catch (error: any) { 76 | handleErrors(error); 77 | } 78 | 79 | return user; 80 | } 81 | 82 | @Mutation(() => User) 83 | async updateUser( 84 | @Arg("id") id: string, 85 | @Arg("data") input: UserInput, 86 | ): Promise { 87 | try { 88 | // update user 89 | await User.update( 90 | { 91 | name: input.name, 92 | email: input.email, 93 | phone: input.phone, 94 | city: input.city, 95 | gender: input.gender, 96 | username: input.username, 97 | password: input.password, 98 | role_id: input.role_id, 99 | }, 100 | { 101 | where: { id: id }, 102 | }, 103 | ); 104 | 105 | if (input.business) { 106 | // update business 107 | await Business.update( 108 | { 109 | name: input.business.name, 110 | description: input.business.description, 111 | address: input.business.address, 112 | status: input.business.status, 113 | username: input.username, 114 | password: input.password, 115 | }, 116 | { 117 | where: { user_id: id }, 118 | }, 119 | ); 120 | } 121 | } catch (error: any) { 122 | handleErrors(error); 123 | } 124 | 125 | return this.findUserByID(id); 126 | } 127 | 128 | @Mutation(() => Boolean) 129 | async deleteUser(@Arg("id") id: number): Promise { 130 | let count = 0; 131 | try { 132 | count = await User.destroy({ where: { id } }); 133 | await Business.destroy({ where: { user_id: id } }); 134 | } catch (error: any) { 135 | handleErrors(error); 136 | } 137 | return count > 0; 138 | } 139 | 140 | async findUserByID(id: string): Promise { 141 | let user: any; 142 | try { 143 | // find user 144 | user = await User.findOne({ 145 | where: { id: id }, 146 | include: { 147 | model: Business, 148 | as: "business", 149 | }, 150 | }); 151 | } catch (error: any) { 152 | handleErrors(error); 153 | } 154 | 155 | if (user == null) { 156 | throw new NotFoundException("User not found for given Id"); 157 | } 158 | 159 | return user; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /backend/src/user/api-doc.md: -------------------------------------------------------------------------------- 1 | # API list 2 | 3 | ## 1. Create user and business 4 | 5 |
    6 | Create user and business 7 |
    8 | 9 | ## Register business user 10 | 11 | - **Description** : This API is used to register the user and its business. 12 | - **Request type** : mutation 13 | - **Request body sample**: 14 | 15 | - **Request body** 16 | 17 | ``` 18 | mutation CreateUser($data: UserInput!) { 19 | createUser(data: $data) { 20 | name 21 | email 22 | phone 23 | city 24 | } 25 | } 26 | ``` 27 | 28 | - **Add variables like below** 29 | 30 | ``` 31 | { 32 | "data": { 33 | "name" : "name", 34 | "city" : "city-name", 35 | "email": "email", 36 | "phone" : "phone", 37 | "business_name": "business name" 38 | }, 39 | } 40 | ``` 41 | 42 | - **Response**: 43 | 44 | ```json 45 | { 46 | "data": { 47 | "createUser": { 48 | "name": "sumi", 49 | "email": "sumi@gmail.com", 50 | "phone": "9999999999", 51 | "city": "surat" 52 | } 53 | } 54 | } 55 | ``` 56 | 57 |
    58 | 59 | ## 2. Get users 60 | 61 |
    62 | Get all users with business 63 |
    64 | 65 | ## Get users 66 | 67 | - **Description** : This API is used to get users who are not admins. 68 | - **Request type** : query 69 | - **Request body sample**: 70 | 71 | - **Request body** 72 | 73 | ``` 74 | query Users { 75 | users { 76 | id 77 | name 78 | email 79 | city 80 | business { 81 | name 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | - **Response**: 88 | 89 | ```json 90 | { 91 | "data": { 92 | "users": [ 93 | { 94 | "id": 1, 95 | "name": "User1", 96 | "email": "user1@example.com", 97 | "city": "city", 98 | "business": { 99 | "name": "MyBusiness" 100 | } 101 | }, 102 | { 103 | "id": 2, 104 | "name": "User2", 105 | "email": "user2@example.com", 106 | "city": "city", 107 | "business": null 108 | } 109 | ] 110 | } 111 | } 112 | ``` 113 | 114 |
    115 | 116 | ## 3. Get user by ID 117 | 118 |
    119 | Get user by given id 120 |
    121 | 122 | ## Get user by ID 123 | 124 | - **Description** : This API is used to get user by given id. 125 | - **Request type** : query 126 | - **Request body sample**: 127 | 128 | - **Request body** 129 | 130 | ``` 131 | query FindUser($id: String!) { 132 | user(id: $id) { 133 | id 134 | name 135 | email 136 | city 137 | business { 138 | name 139 | } 140 | } 141 | } 142 | ``` 143 | 144 | - **Add variable like below** 145 | 146 | ``` 147 | { 148 | "id": "1", 149 | } 150 | ``` 151 | 152 | - **Response**: 153 | 154 | ```json 155 | { 156 | "data": { 157 | "user": { 158 | "id": 1, 159 | "name": "User1", 160 | "email": "user1@example.com", 161 | "city": "city", 162 | "business": { 163 | "name": "MyBusiness" 164 | } 165 | } 166 | } 167 | } 168 | ``` 169 | 170 |
    171 | 172 | ## 4. Update user 173 | 174 |
    175 | Update user 176 |
    177 | 178 | ## Update user 179 | 180 | - **Description** : This API is used to update user. 181 | - **Request type** : mutation 182 | - **Request body sample**: 183 | 184 | - **Request body** 185 | 186 | ``` 187 | mutation UpdateUser($id: String!, $data: UserInput!) { 188 | updateUser(id: $id, data: $data) { 189 | name 190 | email 191 | } 192 | } 193 | ``` 194 | 195 | - **Add variables like below** 196 | 197 | ``` 198 | { 199 | "id": "1", 200 | "data": { 201 | "email": "user2@example.com", 202 | }, 203 | } 204 | ``` 205 | 206 | - **Response**: 207 | 208 | ```json 209 | { 210 | "data": { 211 | "updateUser": { 212 | "name": "User1", 213 | "email": "user2@example.com" 214 | } 215 | } 216 | } 217 | ``` 218 | 219 |
    220 | 221 | ## 5. Delete user 222 | 223 |
    224 | Delete user 225 |
    226 | 227 | ## Delete user 228 | 229 | - **Description** : This API is used to delete user. 230 | - **Request type** : mutation 231 | - **Request body sample**: 232 | 233 | - **Request body** 234 | 235 | ``` 236 | mutation DeleteUser($id: Float!) { 237 | deleteUser(id: $id) 238 | } 239 | ``` 240 | 241 | - **Add variables like below** 242 | 243 | ``` 244 | { 245 | "id": "1", 246 | } 247 | ``` 248 | 249 | - **Response**: 250 | 251 | ```json 252 | { 253 | "data": { 254 | "deleteUser": true 255 | } 256 | } 257 | ``` 258 | 259 |
    260 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import { NavLink, useLocation } from "react-router-dom"; 4 | import { 5 | faFileCirclePlus, 6 | faPenClip, 7 | faUserGroup, 8 | } from "@fortawesome/free-solid-svg-icons"; 9 | 10 | interface SidebarProps { 11 | sidebarOpen: boolean; 12 | setSidebarOpen: (arg: boolean) => void; 13 | } 14 | 15 | const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => { 16 | const location = useLocation(); 17 | const { pathname } = location; 18 | 19 | const trigger = useRef(null); 20 | const sidebar = useRef(null); 21 | 22 | const storedSidebarExpanded = localStorage.getItem("sidebar-expanded"); 23 | const [sidebarExpanded] = useState( 24 | storedSidebarExpanded === null ? false : storedSidebarExpanded === "true", 25 | ); 26 | 27 | // close on click outside 28 | useEffect(() => { 29 | const clickHandler = ({ target }: MouseEvent) => { 30 | if (!sidebar.current || !trigger.current) return; 31 | if ( 32 | !sidebarOpen || 33 | sidebar.current.contains(target) || 34 | trigger.current.contains(target) 35 | ) 36 | return; 37 | setSidebarOpen(false); 38 | }; 39 | document.addEventListener("click", clickHandler); 40 | return () => document.removeEventListener("click", clickHandler); 41 | }); 42 | 43 | // close if the esc key is pressed 44 | useEffect(() => { 45 | const keyHandler = ({ keyCode }: KeyboardEvent) => { 46 | if (!sidebarOpen || keyCode !== 27) return; 47 | setSidebarOpen(false); 48 | }; 49 | document.addEventListener("keydown", keyHandler); 50 | return () => document.removeEventListener("keydown", keyHandler); 51 | }); 52 | 53 | useEffect(() => { 54 | localStorage.setItem("sidebar-expanded", sidebarExpanded.toString()); 55 | if (sidebarExpanded) { 56 | document.querySelector("body")?.classList.add("sidebar-expanded"); 57 | } else { 58 | document.querySelector("body")?.classList.remove("sidebar-expanded"); 59 | } 60 | }, [sidebarExpanded]); 61 | 62 | return ( 63 | 126 | ); 127 | }; 128 | 129 | export default Sidebar; 130 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Categories/Create.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/client"; 2 | import DefaultLayout from "../../layout/DefaultLayout"; 3 | import Breadcrumb from "../Breadcrumb"; 4 | import { messages } from "./../../config/const"; 5 | import { useNavigate } from "react-router-dom"; 6 | import { useState } from "react"; 7 | import { CREATE_CATEGORY } from "../../graphQL/mutations"; 8 | import ErrorAlert from "../Alerts/Error"; 9 | import { useSelector } from "react-redux"; 10 | 11 | const CategoryCreate = () => { 12 | const navigate = useNavigate(); 13 | const [modalVisible, setModalVisible] = useState(false); 14 | const [modalContent, setModalContent] = useState(""); 15 | const [modalTitle, setModalTitle] = useState(""); 16 | const [formState, setFormState] = useState({ 17 | id: 0, 18 | name: "", 19 | parent_id: 0, 20 | }); 21 | 22 | const [createCategory] = useMutation(CREATE_CATEGORY); 23 | const data = useSelector((state: any) => state.category.categories); 24 | 25 | const categories = data.filter( 26 | (category: any) => !category.parent_id || category.parent_id == 0, 27 | ); 28 | 29 | const handleSubmit = async (e: any) => { 30 | e.preventDefault(); 31 | 32 | try { 33 | await createCategory({ 34 | variables: { 35 | data: { 36 | name: formState.name, 37 | parent_id: formState.parent_id, 38 | business_id: localStorage.getItem("businessId"), 39 | }, 40 | }, 41 | }); 42 | navigate("/categories", { state: { prevAction: "create" } }); 43 | } catch (error: any) { 44 | setModalVisible(true); 45 | setModalTitle("Error"); 46 | setModalContent(error.message || messages.ERROR); 47 | } 48 | }; 49 | 50 | return ( 51 | 52 | 53 | 54 | {modalVisible ? ( 55 | 60 | ) : ( 61 | "" 62 | )} 63 |
    64 |
    65 |

    66 | Create Category 67 |

    68 |
    69 | 70 |
    { 72 | handleSubmit(e); 73 | }} 74 | > 75 |
    76 |
    77 |
    78 | 81 | 85 | setFormState({ 86 | ...formState, 87 | name: e.target.value, 88 | }) 89 | } 90 | placeholder="Enter your first name" 91 | required 92 | className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-desaturatedBlue dark:bg-darkDesaturatedBlue dark:focus:border-primary" 93 | /> 94 |
    95 | 96 | {categories.length > 0 ? ( 97 |
    98 | 101 | 118 |
    119 | ) : ( 120 | "" 121 | )} 122 |
    123 | 124 | 130 |
    131 |
    132 |
    133 |
    134 | ); 135 | }; 136 | 137 | export default CategoryCreate; 138 | -------------------------------------------------------------------------------- /backend/src/business_user/resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Arg, Query, Mutation } from "type-graphql"; 2 | import User from "../user/model"; 3 | import Business from "../business/model"; 4 | import { roles, statusCodes } from "../config/const.config"; 5 | import BusinessUser, { BusinessUserInput, LoginInput } from "./model"; 6 | import { handleErrors } from "../util/handlers.util"; 7 | import { ApolloServerErrorCode } from "@apollo/server/errors"; 8 | import BadRequestException from "../exceptions/BadRequestException"; 9 | import bcrypt from "bcryptjs"; 10 | import dotenv from "dotenv"; 11 | import NotFoundException from "../exceptions/NotFoundException"; 12 | dotenv.config(); 13 | 14 | @Resolver(() => BusinessUser) 15 | export class BusinessUserResolver { 16 | @Query(() => [BusinessUser]) 17 | async businessUsers( 18 | @Arg("businessId") businessId: string, 19 | ): Promise<[BusinessUser]> { 20 | let users: any; 21 | try { 22 | users = await BusinessUser.findAll({ 23 | where: { business_id: businessId }, 24 | }); 25 | } catch (error: any) { 26 | handleErrors(error); 27 | } 28 | return users; 29 | } 30 | 31 | @Query(() => BusinessUser) 32 | async businessUser(@Arg("id") id: string): Promise { 33 | return this.findUserByID(id); 34 | } 35 | 36 | @Mutation(() => Boolean) 37 | async setBusinessDetails( 38 | @Arg("businessId") businessId: string, 39 | ): Promise { 40 | let user: any = await this.findBusinessUser(businessId); 41 | try { 42 | // create business user 43 | await BusinessUser.create({ 44 | name: user.name, 45 | email: user.email, 46 | role_id: roles.OWNER, 47 | business_id: businessId, 48 | username: user.username, 49 | password: user.password, 50 | }); 51 | } catch (error: any) { 52 | handleErrors(error); 53 | } 54 | 55 | return true; 56 | } 57 | 58 | @Mutation(() => BusinessUser) 59 | async login(@Arg("data") input: LoginInput): Promise { 60 | let user: any = null; 61 | try { 62 | const existingUser = await BusinessUser.findOne({ 63 | where: { username: input.username, business_id: input.businessId }, 64 | }); 65 | 66 | if (existingUser?.password) { 67 | existingUser.password = 68 | existingUser.role_id !== roles.OWNER 69 | ? bcrypt.hashSync(existingUser.password, process.env.PASSWORD_SALT) 70 | : existingUser.password; 71 | 72 | user = existingUser.password == input.password ? existingUser : user; 73 | } 74 | } catch (error: any) { 75 | handleErrors(error); 76 | } 77 | 78 | if (user == null) { 79 | handleErrors({ 80 | code: statusCodes.UNAUTHORIZED, 81 | errors: [{ message: "Invalid credentials!!" }], 82 | }); 83 | } 84 | 85 | return user; 86 | } 87 | 88 | @Mutation(() => BusinessUser) 89 | async createBusinessUser( 90 | @Arg("data") input: BusinessUserInput, 91 | ): Promise { 92 | if (!input.username || input.username == "") { 93 | throw new BadRequestException( 94 | "Username is required!", 95 | ApolloServerErrorCode.BAD_REQUEST, 96 | ); 97 | } 98 | 99 | let user: any; 100 | try { 101 | // create user 102 | user = await BusinessUser.create({ 103 | name: input.name, 104 | email: input.email, 105 | role_id: input.role_id, 106 | business_id: input.business_id, 107 | username: input.username, 108 | password: input.password, 109 | }); 110 | } catch (error: any) { 111 | handleErrors(error); 112 | } 113 | 114 | return user; 115 | } 116 | 117 | @Mutation(() => BusinessUser) 118 | async updateBusinessUser( 119 | @Arg("id") id: string, 120 | @Arg("data") input: BusinessUserInput, 121 | ): Promise { 122 | try { 123 | // update user 124 | await BusinessUser.update( 125 | { 126 | name: input.name, 127 | email: input.email, 128 | username: input.username, 129 | password: input.password, 130 | role_id: input.role_id, 131 | }, 132 | { 133 | where: { id: id }, 134 | }, 135 | ); 136 | } catch (error: any) { 137 | handleErrors(error); 138 | } 139 | 140 | return this.findUserByID(id); 141 | } 142 | 143 | @Mutation(() => Boolean) 144 | async deleteBusinessUser(@Arg("id") id: number): Promise { 145 | let count = 0; 146 | try { 147 | count = await BusinessUser.destroy({ where: { id } }); 148 | } catch (error: any) { 149 | handleErrors(error); 150 | } 151 | return count > 0; 152 | } 153 | 154 | async findBusinessUser(businessId: string): Promise { 155 | let user: any; 156 | try { 157 | user = await User.findOne({ 158 | include: { 159 | model: Business, 160 | as: "business", 161 | where: { link_id: businessId }, 162 | }, 163 | }); 164 | } catch (error: any) { 165 | handleErrors(error); 166 | } 167 | 168 | return user; 169 | } 170 | 171 | async findUserByID(id: string): Promise { 172 | let user: any; 173 | try { 174 | // find user 175 | user = await BusinessUser.findOne({ 176 | where: { id: id }, 177 | }); 178 | } catch (error: any) { 179 | handleErrors(error); 180 | } 181 | 182 | if (user == null) { 183 | throw new NotFoundException(); 184 | } 185 | 186 | return user; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Categories/Edit.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/client"; 2 | import DefaultLayout from "../../layout/DefaultLayout"; 3 | import Breadcrumb from "../Breadcrumb"; 4 | import { messages } from "./../../config/const"; 5 | import { useNavigate, useParams } from "react-router-dom"; 6 | import { useEffect, useState } from "react"; 7 | import { UPDATE_CATEGORY } from "../../graphQL/mutations"; 8 | import ErrorAlert from "../Alerts/Error"; 9 | import { useSelector } from "react-redux"; 10 | 11 | const CategoryEdit = () => { 12 | let { id } = useParams(); 13 | const navigate = useNavigate(); 14 | const [modalVisible, setModalVisible] = useState(false); 15 | const [modalContent, setModalContent] = useState(""); 16 | const [modalTitle, setModalTitle] = useState(""); 17 | const [formState, setFormState] = useState({ 18 | id: 0, 19 | name: "", 20 | parent_id: 0, 21 | }); 22 | 23 | const [updateCategory] = useMutation(UPDATE_CATEGORY); 24 | 25 | let data = useSelector((state: any) => state.category.categories); 26 | 27 | const categories = data.filter( 28 | (category: any) => 29 | (!category.parent_id || category.parent_id == 0) && category.id != id, 30 | ); 31 | 32 | useEffect(() => { 33 | if (data) { 34 | const category = data.filter((category: any) => category.id == id); 35 | 36 | setFormState({ 37 | id: category[0].id, 38 | name: category[0].name, 39 | parent_id: category[0].parent_id, 40 | }); 41 | } 42 | }, [data]); 43 | 44 | const handleSubmit = async (e: any) => { 45 | e.preventDefault(); 46 | 47 | try { 48 | await updateCategory({ 49 | variables: { 50 | id: id, 51 | data: { 52 | name: formState.name, 53 | parent_id: formState.parent_id, 54 | business_id: localStorage.getItem("businessId"), 55 | }, 56 | }, 57 | }); 58 | navigate("/categories"); 59 | } catch (error: any) { 60 | setModalVisible(true); 61 | setModalTitle("Error"); 62 | setModalContent(error.message || messages.ERROR); 63 | } 64 | }; 65 | 66 | return ( 67 | 68 | 69 | {modalVisible ? ( 70 | 75 | ) : ( 76 | "" 77 | )} 78 |
    79 |
    80 |

    81 | Edit Category 82 |

    83 |
    84 | 85 |
    { 87 | handleSubmit(e); 88 | }} 89 | > 90 |
    91 |
    92 |
    93 | 96 | 100 | setFormState({ 101 | ...formState, 102 | name: e.target.value, 103 | }) 104 | } 105 | placeholder="Enter your first name" 106 | required 107 | className="w-full rounded border-[1.5px] border-stroke bg-transparent px-5 py-3 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-desaturatedBlue dark:bg-darkDesaturatedBlue dark:focus:border-primary" 108 | /> 109 |
    110 |
    111 | 112 |
    113 | {categories.length > 0 ? ( 114 |
    115 | 118 | 135 |
    136 | ) : ( 137 | "" 138 | )} 139 |
    140 | 141 | 147 |
    148 |
    149 |
    150 |
    151 | ); 152 | }; 153 | 154 | export default CategoryEdit; 155 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Users/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery } from "@apollo/client"; 2 | import { GET_USERS } from "../../graphQL/queries.jsx"; 3 | import { DELETE_USER } from "../../graphQL/mutations.jsx"; 4 | import DefaultLayout from "../../layout/DefaultLayout.jsx"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; 7 | import { library } from "@fortawesome/fontawesome-svg-core"; 8 | import { confirmAlert } from "react-confirm-alert"; 9 | import "react-confirm-alert/src/react-confirm-alert.css"; 10 | import ErrorAlert from "../Alerts/Error.jsx"; 11 | import { useState } from "react"; 12 | import { Link } from "react-router-dom"; 13 | import useLocalStorage from "../../hooks/useLocalStorage.js"; 14 | 15 | library.add(faEdit, faTrash); 16 | 17 | const Users = () => { 18 | const [loggedUser]: any = useLocalStorage("user", ""); 19 | const [errorAlert, setErrorAlert] = useState(false); 20 | const [modalContent, setModalContent] = useState(""); 21 | const [modalTitle, setModalTitle] = useState(""); 22 | const [destoryUser] = useMutation(DELETE_USER); 23 | const { data, refetch }: any = useQuery(GET_USERS, { 24 | variables: { 25 | businessId: localStorage.getItem("businessId"), 26 | }, 27 | }); 28 | 29 | let users: any = data ? data.businessUsers : []; 30 | 31 | const handleDelete = (id: number) => { 32 | confirmAlert({ 33 | title: "Confirm to delete", 34 | message: "Are you sure to delete this user?", 35 | buttons: [ 36 | { 37 | label: "Yes", 38 | onClick: () => deleteUser(id), 39 | }, 40 | { 41 | label: "No", 42 | onClick: () => { 43 | return; 44 | }, 45 | }, 46 | ], 47 | }); 48 | }; 49 | 50 | const deleteUser = (id: number) => { 51 | destoryUser({ 52 | variables: { 53 | id: id, 54 | }, 55 | }) 56 | .then((result) => { 57 | if (!result.data.deleteUser) { 58 | setErrorAlert(true); 59 | setModalTitle("Error"); 60 | setModalContent("Entry not found!! Please try other data."); 61 | } 62 | refetch(); 63 | }) 64 | .catch((error: any) => { 65 | setErrorAlert(true); 66 | setModalTitle("Error"); 67 | setModalContent( 68 | "Error deleting item: " + error.message ? error.message : "", 69 | ); 70 | }); 71 | }; 72 | 73 | return ( 74 | 75 |
    76 |
    77 |

    78 | Users 79 |

    80 | 81 | 85 | 86 | 87 |
    88 | 89 |
    90 |
    91 |
    92 |
    93 | Name 94 |
    95 |
    96 |
    97 |
    98 | Email 99 |
    100 |
    101 |
    102 |
    103 | Username 104 |
    105 |
    106 |
    107 |
    108 | Actions 109 |
    110 |
    111 |
    112 | {errorAlert ? ( 113 | 118 | ) : ( 119 | "" 120 | )} 121 | {users 122 | ? users.map(function (user: any) { 123 | return ( 124 |
    128 |
    129 |

    {user.name}

    130 |
    131 | 132 |
    133 |

    {user.email}

    134 |
    135 | 136 |
    137 |

    138 | {user.username} 139 |

    140 |
    141 | 142 | {user.id != loggedUser.id ? ( 143 |
    144 | 145 | 146 | 147 | handleDelete(user.id)} 150 | /> 151 |
    152 | ) : ( 153 | "" 154 | )} 155 |
    156 | ); 157 | }) 158 | : ""} 159 |
    160 |
    161 |
    162 | ); 163 | }; 164 | 165 | export default Users; 166 | -------------------------------------------------------------------------------- /business-dashboard/src/components/Categories/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery } from "@apollo/client"; 2 | import { GET_CATEGORIES } from "../../graphQL/queries.jsx"; 3 | import { DELETE_CATEGORY } from "../../graphQL/mutations.jsx"; 4 | import DefaultLayout from "../../layout/DefaultLayout.jsx"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; 7 | import { library } from "@fortawesome/fontawesome-svg-core"; 8 | import { confirmAlert } from "react-confirm-alert"; 9 | import "react-confirm-alert/src/react-confirm-alert.css"; 10 | import ErrorAlert from "../Alerts/Error.jsx"; 11 | import { useState } from "react"; 12 | import { Link, useLocation } from "react-router-dom"; 13 | import { useDispatch } from "react-redux"; 14 | import { setCategories } from "../../store/categories.js"; 15 | 16 | library.add(faEdit, faTrash); 17 | 18 | const Categories = () => { 19 | const dispatch = useDispatch(); 20 | const [errorAlert, setErrorAlert] = useState(false); 21 | const [modalContent, setModalContent] = useState(""); 22 | const [modalTitle, setModalTitle] = useState(""); 23 | const [destoryCategory] = useMutation(DELETE_CATEGORY); 24 | const { data, refetch }: any = useQuery(GET_CATEGORIES, { 25 | variables: { 26 | businessId: localStorage.getItem("businessId"), 27 | }, 28 | }); 29 | 30 | const { state } = useLocation(); 31 | if (state) { 32 | const { prevAction } = state ? state : null; // Read values passed on state 33 | if (prevAction == "create") { 34 | refetch(); 35 | } 36 | } 37 | 38 | let categories: any = data ? data.categories : []; 39 | dispatch(setCategories(categories)); 40 | 41 | const handleDelete = (id: number) => { 42 | confirmAlert({ 43 | title: "Confirm to delete", 44 | message: "Are you sure to delete this?", 45 | buttons: [ 46 | { 47 | label: "Yes", 48 | onClick: () => deleteCategory(id), 49 | }, 50 | { 51 | label: "No", 52 | onClick: () => { 53 | return; 54 | }, 55 | }, 56 | ], 57 | }); 58 | }; 59 | 60 | const deleteCategory = (id: number) => { 61 | destoryCategory({ 62 | variables: { 63 | id: id, 64 | }, 65 | }) 66 | .then((result) => { 67 | if (!result.data.deleteCategory) { 68 | setErrorAlert(true); 69 | setModalTitle("Error"); 70 | setModalContent("Entry not found!! Please try other data."); 71 | } 72 | refetch(); 73 | }) 74 | .catch((error: any) => { 75 | setErrorAlert(true); 76 | setModalTitle("Error"); 77 | setModalContent( 78 | "Error deleting item: " + error.message ? error.message : "", 79 | ); 80 | }); 81 | }; 82 | 83 | const mainCategoryName = (parentId: number): string => { 84 | let parentCat = categories.filter((cat: any) => cat.id == parentId)[0]; 85 | return parentCat?.name || "None"; 86 | }; 87 | 88 | return ( 89 | 90 |
    91 |
    92 |

    93 | Categories 94 |

    95 | 96 | 100 | 101 | 102 |
    103 | 104 |
    105 |
    106 |
    107 |
    108 | Name 109 |
    110 |
    111 |
    112 |
    113 | Subcategory Of 114 |
    115 |
    116 |
    117 |
    118 | Actions 119 |
    120 |
    121 |
    122 | {errorAlert ? ( 123 | 128 | ) : ( 129 | "" 130 | )} 131 | {categories 132 | ? categories.map(function (category: any) { 133 | return ( 134 |
    138 |
    139 |

    140 | {category.name} 141 |

    142 |
    143 | 144 |
    145 |

    146 | {mainCategoryName(category.parent_id)} 147 |

    148 |
    149 | 150 |
    151 | 152 | 153 | 154 | handleDelete(category.id)} 157 | /> 158 |
    159 |
    160 | ); 161 | }) 162 | : ""} 163 |
    164 |
    165 |
    166 | ); 167 | }; 168 | 169 | export default Categories; 170 | -------------------------------------------------------------------------------- /business-dashboard/src/pages/Authentication/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { useNavigate } from "react-router-dom"; 4 | import ErrorAlert from "../../components/Alerts/Error"; 5 | import bcrypt from "bcryptjs"; 6 | import { messages } from "../../config/const"; 7 | import Business from "../../assets/images/user/business.webp"; 8 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 9 | import { faEnvelope, faLock } from "@fortawesome/free-solid-svg-icons"; 10 | import { LOGIN } from "../../graphQL/mutations"; 11 | 12 | const SignIn = () => { 13 | const user = { 14 | username: "", 15 | password: "", 16 | }; 17 | 18 | const [formState, setFormState] = useState(user); 19 | const [modalVisible, setModalVisible] = useState(false); 20 | const [modalContent, setModalContent] = useState(""); 21 | const [modalTitle, setModalTitle] = useState(""); 22 | const [login] = useMutation(LOGIN); 23 | const navigate = useNavigate(); 24 | 25 | const signIn = async (e: any) => { 26 | e.preventDefault(); 27 | try { 28 | let data: any = await login({ 29 | variables: { 30 | data: { 31 | username: formState.username, 32 | businessId: localStorage.getItem("businessId"), 33 | password: bcrypt.hashSync( 34 | formState.password, 35 | import.meta.env.VITE_PASSWORD_SALT, 36 | ), 37 | }, 38 | }, 39 | }); 40 | if (data.data.login.username == "") { 41 | setModalVisible(true); 42 | setModalTitle("Error"); 43 | setModalContent("Invalid credentials!! Please try again."); 44 | } else { 45 | localStorage.setItem("user", JSON.stringify(data.data.login)); 46 | navigate("/"); 47 | } 48 | } catch (error: any) { 49 | setModalVisible(true); 50 | setModalTitle("Error"); 51 | setModalContent(error.message || messages.ERROR); 52 | } 53 | setFormState(user); 54 | }; 55 | 56 | return ( 57 |
    58 |
    59 | {modalVisible ? ( 60 | 65 | ) : ( 66 | "" 67 | )} 68 |
    69 |
    70 |
    71 |
    72 |

    73 | Your business dashboard 74 |

    75 | 76 | business 77 | 78 |
    79 |
    80 | 81 |
    82 |
    83 |

    84 | Sign In 85 |

    86 | 87 |
    { 89 | signIn(e); 90 | }} 91 | > 92 |
    93 | 96 |
    97 | 103 | setFormState({ 104 | ...formState, 105 | username: e.target.value, 106 | }) 107 | } 108 | /> 109 | 110 | 111 | 112 | 113 |
    114 |
    115 | 116 |
    117 | 120 |
    121 | 127 | setFormState({ 128 | ...formState, 129 | password: e.target.value, 130 | }) 131 | } 132 | /> 133 | 134 | 135 | 136 | 137 |
    138 |
    139 | 140 |
    141 | 146 |
    147 |
    148 |
    149 |
    150 |
    151 |
    152 |
    153 |
    154 | ); 155 | }; 156 | 157 | export default SignIn; 158 | -------------------------------------------------------------------------------- /react-admin/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 6 | darkMode: "class", 7 | theme: { 8 | colors: { 9 | transparent: "transparent", 10 | white: "#FFFFFF", 11 | black: "#1C2434", 12 | body: "#64748B", 13 | grayBlue: "#AEB7C0", 14 | lightGrayBlue: "#DEE4EE", 15 | primary: "#3C50E0", 16 | stroke: "#E2E8F0", 17 | gray: "#EFF4FB", 18 | graydark: "#333A48", 19 | grayLight: "#F7F9FC", 20 | whiten: "#F1F5F9", 21 | whiter: "#F5F7FD", 22 | boxdark: "#24303F", 23 | blackBlue: "#1A222C", 24 | strokedark: "#2E3A47", 25 | desaturatedBlue: "#3d4d60", 26 | darkDesaturatedBlue: "#1d2a39", 27 | brightRed: "#DC3545", 28 | darkGrayBlue: "#313D4A", 29 | lightRed: "#FF6766", 30 | success: "#219653", 31 | danger: "#D34053", 32 | warning: "#FFA70B", 33 | }, 34 | screens: { 35 | "2xsm": "375px", 36 | xsm: "425px", 37 | "3xl": "2000px", 38 | ...defaultTheme.screens, 39 | }, 40 | extend: { 41 | fontSize: { 42 | "title-xxl": ["44px", "55px"], 43 | "title-xl": ["36px", "45px"], 44 | "title-xl2": ["33px", "45px"], 45 | "title-lg": ["28px", "35px"], 46 | "title-md": ["24px", "30px"], 47 | "title-md2": ["26px", "30px"], 48 | "title-sm": ["20px", "26px"], 49 | "title-xsm": ["18px", "24px"], 50 | }, 51 | spacing: { 52 | 4.5: "1.125rem", 53 | 5.5: "1.375rem", 54 | 6.5: "1.625rem", 55 | 7.5: "1.875rem", 56 | 8.5: "2.125rem", 57 | 9.5: "2.375rem", 58 | 10.5: "2.625rem", 59 | 11: "2.75rem", 60 | 11.5: "2.875rem", 61 | 12.5: "3.125rem", 62 | 13: "3.25rem", 63 | 13.5: "3.375rem", 64 | 14: "3.5rem", 65 | 14.5: "3.625rem", 66 | 15: "3.75rem", 67 | 15.5: "3.875rem", 68 | 16: "4rem", 69 | 16.5: "4.125rem", 70 | 17: "4.25rem", 71 | 17.5: "4.375rem", 72 | 18: "4.5rem", 73 | 18.5: "4.625rem", 74 | 19: "4.75rem", 75 | 19.5: "4.875rem", 76 | 21: "5.25rem", 77 | 21.5: "5.375rem", 78 | 22: "5.5rem", 79 | 22.5: "5.625rem", 80 | 24.5: "6.125rem", 81 | 25: "6.25rem", 82 | 25.5: "6.375rem", 83 | 26: "6.5rem", 84 | 27: "6.75rem", 85 | 27.5: "6.875rem", 86 | 29: "7.25rem", 87 | 29.5: "7.375rem", 88 | 30: "7.5rem", 89 | 31: "7.75rem", 90 | 32.5: "8.125rem", 91 | 34: "8.5rem", 92 | 34.5: "8.625rem", 93 | 35: "8.75rem", 94 | 36.5: "9.125rem", 95 | 37.5: "9.375rem", 96 | 39: "9.75rem", 97 | 39.5: "9.875rem", 98 | 40: "10rem", 99 | 42.5: "10.625rem", 100 | 44: "11rem", 101 | 45: "11.25rem", 102 | 46: "11.5rem", 103 | 47.5: "11.875rem", 104 | 49: "12.25rem", 105 | 50: "12.5rem", 106 | 52: "13rem", 107 | 52.5: "13.125rem", 108 | 54: "13.5rem", 109 | 54.5: "13.625rem", 110 | 55: "13.75rem", 111 | 55.5: "13.875rem", 112 | 59: "14.75rem", 113 | 60: "15rem", 114 | 62.5: "15.625rem", 115 | 65: "16.25rem", 116 | 67: "16.75rem", 117 | 67.5: "16.875rem", 118 | 70: "17.5rem", 119 | 72.5: "18.125rem", 120 | 73: "18.25rem", 121 | 75: "18.75rem", 122 | 90: "22.5rem", 123 | 94: "23.5rem", 124 | 95: "23.75rem", 125 | 100: "25rem", 126 | 115: "28.75rem", 127 | 125: "31.25rem", 128 | 132.5: "33.125rem", 129 | 150: "37.5rem", 130 | 171.5: "42.875rem", 131 | 180: "45rem", 132 | 187.5: "46.875rem", 133 | 203: "50.75rem", 134 | 230: "57.5rem", 135 | 242.5: "60.625rem", 136 | }, 137 | maxWidth: { 138 | 2.5: "0.625rem", 139 | 3: "0.75rem", 140 | 4: "1rem", 141 | 11: "2.75rem", 142 | 13: "3.25rem", 143 | 14: "3.5rem", 144 | 15: "3.75rem", 145 | 22.5: "5.625rem", 146 | 25: "6.25rem", 147 | 30: "7.5rem", 148 | 34: "8.5rem", 149 | 35: "8.75rem", 150 | 40: "10rem", 151 | 42.5: "10.625rem", 152 | 44: "11rem", 153 | 45: "11.25rem", 154 | 70: "17.5rem", 155 | 90: "22.5rem", 156 | 94: "23.5rem", 157 | 125: "31.25rem", 158 | 132.5: "33.125rem", 159 | 142.5: "35.625rem", 160 | 150: "37.5rem", 161 | 180: "45rem", 162 | 203: "50.75rem", 163 | 230: "57.5rem", 164 | 242.5: "60.625rem", 165 | 270: "67.5rem", 166 | 280: "70rem", 167 | 292.5: "73.125rem", 168 | }, 169 | maxHeight: { 170 | 35: "8.75rem", 171 | 70: "17.5rem", 172 | 90: "22.5rem", 173 | 550: "34.375rem", 174 | 300: "18.75rem", 175 | }, 176 | minWidth: { 177 | 22.5: "5.625rem", 178 | 42.5: "10.625rem", 179 | 47.5: "11.875rem", 180 | 75: "18.75rem", 181 | }, 182 | zIndex: { 183 | 999999: "999999", 184 | 99999: "99999", 185 | 9999: "9999", 186 | 999: "999", 187 | 99: "99", 188 | 9: "9", 189 | 1: "1", 190 | }, 191 | opacity: { 192 | 65: ".65", 193 | }, 194 | transitionProperty: { width: "width", stroke: "stroke" }, 195 | borderWidth: { 196 | 6: "6px", 197 | }, 198 | boxShadow: { 199 | default: "0px 8px 13px -3px rgba(0, 0, 0, 0.07)", 200 | card: "0px 1px 3px rgba(0, 0, 0, 0.12)", 201 | "card-2": "0px 1px 2px rgba(0, 0, 0, 0.05)", 202 | switcher: 203 | "0px 2px 4px rgba(0, 0, 0, 0.2), inset 0px 2px 2px #FFFFFF, inset 0px -1px 1px rgba(0, 0, 0, 0.1)", 204 | "switch-1": "0px 0px 5px rgba(0, 0, 0, 0.15)", 205 | 1: "0px 1px 3px rgba(0, 0, 0, 0.08)", 206 | 2: "0px 1px 4px rgba(0, 0, 0, 0.12)", 207 | 3: "0px 1px 5px rgba(0, 0, 0, 0.14)", 208 | 4: "0px 4px 10px rgba(0, 0, 0, 0.12)", 209 | 5: "0px 1px 1px rgba(0, 0, 0, 0.15)", 210 | 6: "0px 3px 15px rgba(0, 0, 0, 0.1)", 211 | 7: "-5px 0 0 #313D4A, 5px 0 0 #313D4A", 212 | 8: "1px 0 0 #313D4A, -1px 0 0 #313D4A, 0 1px 0 #313D4A, 0 -1px 0 #313D4A, 0 3px 13px rgb(0 0 0 / 8%)", 213 | }, 214 | dropShadow: { 215 | 1: "0px 1px 0px #E2E8F0", 216 | 2: "0px 1px 4px rgba(0, 0, 0, 0.12)", 217 | }, 218 | keyframes: { 219 | rotating: { 220 | "0%, 100%": { transform: "rotate(360deg)" }, 221 | "50%": { transform: "rotate(0deg)" }, 222 | }, 223 | }, 224 | animation: { 225 | "ping-once": "ping 5s cubic-bezier(0, 0, 0.2, 1)", 226 | rotating: "rotating 30s linear infinite", 227 | "spin-1.5": "spin 1.5s linear infinite", 228 | "spin-2": "spin 2s linear infinite", 229 | "spin-3": "spin 3s linear infinite", 230 | }, 231 | }, 232 | }, 233 | plugins: [], 234 | }; 235 | -------------------------------------------------------------------------------- /business-dashboard/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 6 | darkMode: "class", 7 | theme: { 8 | colors: { 9 | transparent: "transparent", 10 | white: "#FFFFFF", 11 | black: "#1C2434", 12 | body: "#64748B", 13 | grayBlue: "#AEB7C0", 14 | lightGrayBlue: "#DEE4EE", 15 | primary: "#3C50E0", 16 | stroke: "#E2E8F0", 17 | gray: "#EFF4FB", 18 | graydark: "#333A48", 19 | grayLight: "#F7F9FC", 20 | whiten: "#F1F5F9", 21 | whiter: "#F5F7FD", 22 | boxdark: "#24303F", 23 | blackBlue: "#1A222C", 24 | strokedark: "#2E3A47", 25 | desaturatedBlue: "#3d4d60", 26 | darkDesaturatedBlue: "#1d2a39", 27 | brightRed: "#DC3545", 28 | darkGrayBlue: "#313D4A", 29 | lightRed: "#FF6766", 30 | success: "#219653", 31 | danger: "#D34053", 32 | warning: "#FFA70B", 33 | }, 34 | screens: { 35 | "2xsm": "375px", 36 | xsm: "425px", 37 | "3xl": "2000px", 38 | ...defaultTheme.screens, 39 | }, 40 | extend: { 41 | fontSize: { 42 | "title-xxl": ["44px", "55px"], 43 | "title-xl": ["36px", "45px"], 44 | "title-xl2": ["33px", "45px"], 45 | "title-lg": ["28px", "35px"], 46 | "title-md": ["24px", "30px"], 47 | "title-md2": ["26px", "30px"], 48 | "title-sm": ["20px", "26px"], 49 | "title-xsm": ["18px", "24px"], 50 | }, 51 | spacing: { 52 | 4.5: "1.125rem", 53 | 5.5: "1.375rem", 54 | 6.5: "1.625rem", 55 | 7.5: "1.875rem", 56 | 8.5: "2.125rem", 57 | 9.5: "2.375rem", 58 | 10.5: "2.625rem", 59 | 11: "2.75rem", 60 | 11.5: "2.875rem", 61 | 12.5: "3.125rem", 62 | 13: "3.25rem", 63 | 13.5: "3.375rem", 64 | 14: "3.5rem", 65 | 14.5: "3.625rem", 66 | 15: "3.75rem", 67 | 15.5: "3.875rem", 68 | 16: "4rem", 69 | 16.5: "4.125rem", 70 | 17: "4.25rem", 71 | 17.5: "4.375rem", 72 | 18: "4.5rem", 73 | 18.5: "4.625rem", 74 | 19: "4.75rem", 75 | 19.5: "4.875rem", 76 | 21: "5.25rem", 77 | 21.5: "5.375rem", 78 | 22: "5.5rem", 79 | 22.5: "5.625rem", 80 | 24.5: "6.125rem", 81 | 25: "6.25rem", 82 | 25.5: "6.375rem", 83 | 26: "6.5rem", 84 | 27: "6.75rem", 85 | 27.5: "6.875rem", 86 | 29: "7.25rem", 87 | 29.5: "7.375rem", 88 | 30: "7.5rem", 89 | 31: "7.75rem", 90 | 32.5: "8.125rem", 91 | 34: "8.5rem", 92 | 34.5: "8.625rem", 93 | 35: "8.75rem", 94 | 36.5: "9.125rem", 95 | 37.5: "9.375rem", 96 | 39: "9.75rem", 97 | 39.5: "9.875rem", 98 | 40: "10rem", 99 | 42.5: "10.625rem", 100 | 44: "11rem", 101 | 45: "11.25rem", 102 | 46: "11.5rem", 103 | 47.5: "11.875rem", 104 | 49: "12.25rem", 105 | 50: "12.5rem", 106 | 52: "13rem", 107 | 52.5: "13.125rem", 108 | 54: "13.5rem", 109 | 54.5: "13.625rem", 110 | 55: "13.75rem", 111 | 55.5: "13.875rem", 112 | 59: "14.75rem", 113 | 60: "15rem", 114 | 62.5: "15.625rem", 115 | 65: "16.25rem", 116 | 67: "16.75rem", 117 | 67.5: "16.875rem", 118 | 70: "17.5rem", 119 | 72.5: "18.125rem", 120 | 73: "18.25rem", 121 | 75: "18.75rem", 122 | 90: "22.5rem", 123 | 94: "23.5rem", 124 | 95: "23.75rem", 125 | 100: "25rem", 126 | 115: "28.75rem", 127 | 125: "31.25rem", 128 | 132.5: "33.125rem", 129 | 150: "37.5rem", 130 | 171.5: "42.875rem", 131 | 180: "45rem", 132 | 187.5: "46.875rem", 133 | 203: "50.75rem", 134 | 230: "57.5rem", 135 | 242.5: "60.625rem", 136 | }, 137 | maxWidth: { 138 | 2.5: "0.625rem", 139 | 3: "0.75rem", 140 | 4: "1rem", 141 | 11: "2.75rem", 142 | 13: "3.25rem", 143 | 14: "3.5rem", 144 | 15: "3.75rem", 145 | 22.5: "5.625rem", 146 | 25: "6.25rem", 147 | 30: "7.5rem", 148 | 34: "8.5rem", 149 | 35: "8.75rem", 150 | 40: "10rem", 151 | 42.5: "10.625rem", 152 | 44: "11rem", 153 | 45: "11.25rem", 154 | 70: "17.5rem", 155 | 90: "22.5rem", 156 | 94: "23.5rem", 157 | 125: "31.25rem", 158 | 132.5: "33.125rem", 159 | 142.5: "35.625rem", 160 | 150: "37.5rem", 161 | 180: "45rem", 162 | 203: "50.75rem", 163 | 230: "57.5rem", 164 | 242.5: "60.625rem", 165 | 270: "67.5rem", 166 | 280: "70rem", 167 | 292.5: "73.125rem", 168 | }, 169 | maxHeight: { 170 | 35: "8.75rem", 171 | 70: "17.5rem", 172 | 90: "22.5rem", 173 | 550: "34.375rem", 174 | 300: "18.75rem", 175 | }, 176 | minWidth: { 177 | 22.5: "5.625rem", 178 | 42.5: "10.625rem", 179 | 47.5: "11.875rem", 180 | 75: "18.75rem", 181 | }, 182 | zIndex: { 183 | 999999: "999999", 184 | 99999: "99999", 185 | 9999: "9999", 186 | 999: "999", 187 | 99: "99", 188 | 9: "9", 189 | 1: "1", 190 | }, 191 | opacity: { 192 | 65: ".65", 193 | }, 194 | transitionProperty: { width: "width", stroke: "stroke" }, 195 | borderWidth: { 196 | 6: "6px", 197 | }, 198 | boxShadow: { 199 | default: "0px 8px 13px -3px rgba(0, 0, 0, 0.07)", 200 | card: "0px 1px 3px rgba(0, 0, 0, 0.12)", 201 | "card-2": "0px 1px 2px rgba(0, 0, 0, 0.05)", 202 | switcher: 203 | "0px 2px 4px rgba(0, 0, 0, 0.2), inset 0px 2px 2px #FFFFFF, inset 0px -1px 1px rgba(0, 0, 0, 0.1)", 204 | "switch-1": "0px 0px 5px rgba(0, 0, 0, 0.15)", 205 | 1: "0px 1px 3px rgba(0, 0, 0, 0.08)", 206 | 2: "0px 1px 4px rgba(0, 0, 0, 0.12)", 207 | 3: "0px 1px 5px rgba(0, 0, 0, 0.14)", 208 | 4: "0px 4px 10px rgba(0, 0, 0, 0.12)", 209 | 5: "0px 1px 1px rgba(0, 0, 0, 0.15)", 210 | 6: "0px 3px 15px rgba(0, 0, 0, 0.1)", 211 | 7: "-5px 0 0 #313D4A, 5px 0 0 #313D4A", 212 | 8: "1px 0 0 #313D4A, -1px 0 0 #313D4A, 0 1px 0 #313D4A, 0 -1px 0 #313D4A, 0 3px 13px rgb(0 0 0 / 8%)", 213 | }, 214 | dropShadow: { 215 | 1: "0px 1px 0px #E2E8F0", 216 | 2: "0px 1px 4px rgba(0, 0, 0, 0.12)", 217 | }, 218 | keyframes: { 219 | rotating: { 220 | "0%, 100%": { transform: "rotate(360deg)" }, 221 | "50%": { transform: "rotate(0deg)" }, 222 | }, 223 | }, 224 | animation: { 225 | "ping-once": "ping 5s cubic-bezier(0, 0, 0.2, 1)", 226 | rotating: "rotating 30s linear infinite", 227 | "spin-1.5": "spin 1.5s linear infinite", 228 | "spin-2": "spin 2s linear infinite", 229 | "spin-3": "spin 3s linear infinite", 230 | }, 231 | }, 232 | }, 233 | plugins: [], 234 | }; 235 | -------------------------------------------------------------------------------- /react-admin/src/pages/Authentication/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { LOGIN } from "../../graphQL/mutations"; 4 | import { Link, useNavigate } from "react-router-dom"; 5 | import ErrorAlert from "../../components/Alerts/Error"; 6 | import bcrypt from "bcryptjs"; 7 | import { messages } from "../../config/const"; 8 | import { validEmail } from "../../config/utils"; 9 | import Business from "../../assets/images/user/business.webp"; 10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 11 | import { faEnvelope, faLock } from "@fortawesome/free-solid-svg-icons"; 12 | 13 | const SignIn = () => { 14 | const user = { 15 | email: "", 16 | password: "", 17 | }; 18 | 19 | const [formState, setFormState] = useState(user); 20 | const [emailError, setEmailError] = useState(""); 21 | const [modalVisible, setModalVisible] = useState(false); 22 | const [modalContent, setModalContent] = useState(""); 23 | const [modalTitle, setModalTitle] = useState(""); 24 | const [login] = useMutation(LOGIN); 25 | const navigate = useNavigate(); 26 | 27 | const signIn = async (e: any) => { 28 | e.preventDefault(); 29 | if (emailError !== "") { 30 | return; 31 | } 32 | 33 | try { 34 | let data: any = await login({ 35 | variables: { 36 | data: { 37 | email: formState.email, 38 | password: bcrypt.hashSync( 39 | formState.password, 40 | import.meta.env.VITE_PASSWORD_SALT, 41 | ), 42 | }, 43 | }, 44 | }); 45 | if (data.data.adminLogin.email == "") { 46 | setModalVisible(true); 47 | setModalTitle("Error"); 48 | setModalContent("Invalid credentials!! Please try again."); 49 | } else { 50 | localStorage.setItem("user", JSON.stringify(data.data.adminLogin)); 51 | navigate("/"); 52 | } 53 | } catch (error: any) { 54 | setModalVisible(true); 55 | setModalTitle("Error"); 56 | setModalContent(error.message || messages.ERROR); 57 | } 58 | setFormState(user); 59 | }; 60 | 61 | return ( 62 |
    63 |
    64 | {modalVisible ? ( 65 | 70 | ) : ( 71 | "" 72 | )} 73 |
    74 |
    75 |
    76 |
    77 |

    78 | Omnidashboard admin 79 |

    80 | 81 | business 82 | 83 |
    84 |
    85 | 86 |
    87 |
    88 |

    89 | Sign In 90 |

    91 | 92 |
    { 94 | signIn(e); 95 | }} 96 | > 97 |
    98 | 101 |
    102 | 108 | setEmailError( 109 | !validEmail(formState.email) 110 | ? "Please enter valid email" 111 | : "", 112 | ) 113 | } 114 | onChange={(e) => 115 | setFormState({ 116 | ...formState, 117 | email: e.target.value, 118 | }) 119 | } 120 | /> 121 | 122 | 123 | 124 | 125 |
    126 |
    127 | 128 |
    129 | 132 |
    133 | 139 | setFormState({ 140 | ...formState, 141 | password: e.target.value, 142 | }) 143 | } 144 | /> 145 | 146 | 147 | 148 | 149 |
    150 |
    151 | 152 |
    153 | 158 |
    159 | 160 |
    161 |

    162 | Don’t have any account?{" "} 163 | 164 | Sign Up 165 | 166 |

    167 |
    168 |
    169 |
    170 |
    171 |
    172 |
    173 |
    174 |
    175 | ); 176 | }; 177 | 178 | export default SignIn; 179 | -------------------------------------------------------------------------------- /backend/src/category/__tests__/category.test.ts: -------------------------------------------------------------------------------- 1 | import { CategoryResolver } from "../resolver"; 2 | import Category, { CategoryInput } from "../model"; 3 | import ServerErrorException from "../../exceptions/ServerErrorException"; 4 | import BadRequestException from "../../exceptions/BadRequestException"; 5 | 6 | describe("CategoryResolver", () => { 7 | let resolver: CategoryResolver; 8 | const businessID = "er34mgni5o"; 9 | 10 | const categories: Category[] = [ 11 | { 12 | id: 1, 13 | name: "category1", 14 | parent_id: 0, 15 | }, 16 | { 17 | id: 2, 18 | name: "category2", 19 | parent_id: 1, 20 | }, 21 | ] as Category[]; 22 | 23 | let input: CategoryInput = {} as CategoryInput; 24 | 25 | beforeAll(() => { 26 | resolver = new CategoryResolver(); 27 | }); 28 | 29 | describe("getCategories", () => { 30 | it("should handle errors while getting categories", async () => { 31 | const findMock = jest 32 | .spyOn(Category, "findAll") 33 | .mockRejectedValueOnce( 34 | new ServerErrorException("An error occurred at server"), 35 | ); 36 | 37 | try { 38 | await resolver.categories(businessID); 39 | } catch (error: any) { 40 | expect(error).toBeInstanceOf(ServerErrorException); 41 | expect(error.message).toBe("An error occurred at server"); 42 | expect(findMock).toHaveBeenCalledTimes(1); 43 | } 44 | 45 | findMock.mockRestore(); // Restore the original implementation 46 | }); 47 | 48 | it("should return categories", async () => { 49 | const findMock = jest 50 | .spyOn(Category, "findAll") 51 | .mockResolvedValueOnce(categories); 52 | 53 | const result = await resolver.categories(businessID); 54 | 55 | expect(result).toEqual(categories); 56 | expect(findMock).toHaveBeenCalledTimes(1); 57 | 58 | findMock.mockRestore(); // Restore the original implementation 59 | }); 60 | }); 61 | 62 | describe("getCategory", () => { 63 | it("should handle errors while getting category", async () => { 64 | const findMock = jest 65 | .spyOn(Category, "findOne") 66 | .mockRejectedValueOnce( 67 | new ServerErrorException("An error occurred at server"), 68 | ); 69 | 70 | try { 71 | await resolver.category("4"); 72 | } catch (error: any) { 73 | expect(error).toBeInstanceOf(ServerErrorException); 74 | expect(error.message).toBe("An error occurred at server"); 75 | expect(findMock).toHaveBeenCalledTimes(1); 76 | } 77 | 78 | findMock.mockRestore(); // Restore the original implementation 79 | }); 80 | 81 | it("should return category", async () => { 82 | const findMock = jest 83 | .spyOn(Category, "findOne") 84 | .mockResolvedValueOnce(categories[0]); 85 | 86 | const result = await resolver.category("1"); 87 | 88 | expect(result).toEqual(categories[0]); 89 | expect(findMock).toHaveBeenCalledTimes(1); 90 | 91 | findMock.mockRestore(); // Restore the original implementation 92 | }); 93 | }); 94 | 95 | describe("createCategory", () => { 96 | it("should handle bad request error for input during category creation", async () => { 97 | try { 98 | await resolver.createCategory(input); 99 | } catch (error: any) { 100 | expect(error).toBeInstanceOf(BadRequestException); 101 | expect(error.message).toBe("Name and business id are required!"); 102 | } 103 | }); 104 | 105 | it("should handle server error during category creation", async () => { 106 | const createMock = jest 107 | .spyOn(Category, "create") 108 | .mockRejectedValueOnce( 109 | new ServerErrorException("An error occurred at server"), 110 | ); 111 | 112 | try { 113 | input.name = "category3"; 114 | input.parent_id = 0; 115 | 116 | await resolver.createCategory(input); 117 | } catch (error: any) { 118 | expect(error).toBeInstanceOf(ServerErrorException); 119 | expect(error.message).toBe("An error occurred at server"); 120 | expect(createMock).toHaveBeenCalledTimes(1); 121 | } 122 | 123 | createMock.mockRestore(); // Restore the original implementation 124 | }); 125 | 126 | it("should create a category and return the created category", async () => { 127 | const cat = { 128 | id: 1, 129 | name: "category3", 130 | business_id: businessID, 131 | parent_id: 0, 132 | }; 133 | const createMock = jest 134 | .spyOn(Category, "create") 135 | .mockResolvedValueOnce(cat); 136 | 137 | input.business_id = businessID; 138 | const result = await resolver.createCategory(input); 139 | 140 | expect(result).toEqual(cat); 141 | expect(createMock).toHaveBeenCalledTimes(1); 142 | 143 | createMock.mockRestore(); // Restore the original implementation 144 | }); 145 | }); 146 | 147 | describe("updateCategory", () => { 148 | it("should handle errors when updating category", async () => { 149 | const updateMock = jest 150 | .spyOn(Category, "update") 151 | .mockRejectedValueOnce( 152 | new ServerErrorException("An error occurred at server"), 153 | ); 154 | 155 | try { 156 | await resolver.updateCategory("1", input); 157 | } catch (error: any) { 158 | expect(error).toBeInstanceOf(ServerErrorException); 159 | expect(error.message).toBe("An error occurred at server"); 160 | expect(updateMock).toHaveBeenCalledTimes(1); 161 | } 162 | 163 | updateMock.mockRestore(); // Restore the original implementation 164 | }); 165 | 166 | it("should update a category", async () => { 167 | const updateMock = jest 168 | .spyOn(Category, "update") 169 | .mockResolvedValueOnce([1]); 170 | 171 | const findMock = jest 172 | .spyOn(Category, "findOne") 173 | .mockResolvedValueOnce(categories[0]); 174 | 175 | const result = await resolver.updateCategory("1", input); 176 | 177 | expect(result).toEqual(categories[0]); 178 | 179 | expect(updateMock).toHaveBeenCalledTimes(1); 180 | expect(findMock).toHaveBeenCalledTimes(1); 181 | 182 | updateMock.mockRestore(); // Restore the original implementation 183 | findMock.mockRestore(); 184 | }); 185 | }); 186 | 187 | describe("deleteCategory", () => { 188 | it("should handle errors when deleting category", async () => { 189 | const findMock = jest 190 | .spyOn(Category, "findAll") 191 | .mockResolvedValueOnce([]); 192 | 193 | const deleteMock = jest 194 | .spyOn(Category, "destroy") 195 | .mockRejectedValueOnce( 196 | new ServerErrorException("An error occurred at server"), 197 | ); 198 | 199 | try { 200 | await resolver.deleteCategory(1); 201 | expect(findMock).toHaveBeenCalledTimes(1); 202 | } catch (error: any) { 203 | expect(error).toBeInstanceOf(ServerErrorException); 204 | expect(error.message).toBe("An error occurred at server"); 205 | expect(deleteMock).toHaveBeenCalledTimes(1); 206 | } 207 | 208 | findMock.mockRestore(); 209 | deleteMock.mockRestore(); // Restore the original implementation 210 | }); 211 | 212 | it("should delete a category", async () => { 213 | const deleteMock = jest 214 | .spyOn(Category, "destroy") 215 | .mockResolvedValueOnce(1); 216 | 217 | const findMock = jest 218 | .spyOn(Category, "findAll") 219 | .mockResolvedValueOnce([]); 220 | 221 | const result = await resolver.deleteCategory(1); 222 | 223 | expect(result).toEqual(true); 224 | 225 | expect(findMock).toHaveBeenCalledTimes(1); 226 | expect(deleteMock).toHaveBeenCalledTimes(1); 227 | 228 | findMock.mockRestore(); // Restore the original implementation 229 | deleteMock.mockRestore(); // Restore the original implementation 230 | }); 231 | }); 232 | }); 233 | --------------------------------------------------------------------------------