├── .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 |
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 |
13 |
14 |
15 | Dashboard /
16 |
17 | {pageName}
18 |
19 |
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 |
13 |
14 |
15 | Dashboard /
16 |
17 | {pageName}
18 |
19 |
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 |
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 |
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 |
15 | {
18 | if (typeof setColorMode === "function") {
19 | setColorMode(colorMode === "light" ? "dark" : "light");
20 | }
21 | }}
22 | className="dur absolute top-0 z-50 m-0 h-full w-full cursor-pointer opacity-0"
23 | />
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
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 |
27 |
28 | setOpen(false)}
32 | >
33 | Close
34 |
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 |
27 |
28 | setOpen(false)}
32 | >
33 | Close
34 |
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 |
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 |
59 |
60 |
61 |
62 |
63 | setOpen(false)}
67 | >
68 | Close
69 |
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 |
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 |
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 | logOut()}
88 | className="flex items-center gap-3.5 px-6 py-4 text-sm font-medium duration-300 ease-in-out hover:text-primary lg:text-base"
89 | >
90 |
94 | Log Out
95 |
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 |
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 | logOut()}
88 | className="flex items-center gap-3.5 px-6 py-4 text-sm font-medium duration-300 ease-in-out hover:text-primary lg:text-base"
89 | >
90 |
94 | Log Out
95 |
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 |
65 | {/* */}
66 |
67 | OmniDashboard
68 |
69 | {/* */}
70 |
71 |
72 | {/* */}
73 |
74 | {/* */}
75 |
76 |
77 | {/* */}
78 |
79 |
80 |
87 |
88 | Users
89 |
90 |
91 | {/* */}
92 |
93 |
94 |
95 | {/* */}
96 |
97 |
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 |
69 | {/* */}
70 |
71 | OmniDashboard
72 |
73 | {/* */}
74 |
75 |
76 | {/* */}
77 |
78 | {/* */}
79 |
80 |
81 | {/* */}
82 |
83 |
84 |
91 |
92 | Users
93 |
94 |
95 |
96 |
103 |
104 | Business details
105 |
106 |
107 |
108 |
115 |
116 | Categories
117 |
118 |
119 | {/* */}
120 |
121 |
122 |
123 | {/* */}
124 |
125 |
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 |
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 |
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 |
131 |
132 |
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 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | Sign In
85 |
86 |
87 |
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 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | Sign In
90 |
91 |
92 |
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 |
--------------------------------------------------------------------------------