commit 449acb7cc9d04aec6f481cbe71cdcdfdffcad5b6 Author: Flatlogic Bot Date: Tue Jul 8 14:53:46 2025 +0000 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e427ff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*/node_modules/ +*/build/ diff --git a/502.html b/502.html new file mode 100644 index 0000000..bbfabeb --- /dev/null +++ b/502.html @@ -0,0 +1,187 @@ + + + + + + + Service Starting + + + + +
+

Loading the app, just a moment…

+

The application is currently launching. The page will automatically refresh once site is + available.

+
+

meng-leap-cash

+

A loan management system for branches.

+
+
+ +
+
+
+ +
+
+ + + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..affcee8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20.15.1-alpine AS builder +RUN apk add --no-cache git +WORKDIR /app +COPY frontend/package.json frontend/yarn.lock ./ +RUN yarn install --pure-lockfile +COPY frontend . +RUN yarn build + +FROM node:20.15.1-alpine +WORKDIR /app +COPY backend/package.json backend/yarn.lock ./ +RUN yarn install --pure-lockfile +COPY backend . + +COPY --from=builder /app/build /app/public +CMD ["yarn", "start"] + diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..aa3b625 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,73 @@ +# Base image for Node.js dependencies +FROM node:20.15.1-alpine AS frontend-deps +RUN apk add --no-cache git +WORKDIR /app/frontend +COPY frontend/package.json frontend/yarn.lock ./ +RUN yarn install --pure-lockfile + +FROM node:20.15.1-alpine AS backend-deps +RUN apk add --no-cache git +WORKDIR /app/backend +COPY backend/package.json backend/yarn.lock ./ +RUN yarn install --pure-lockfile + +FROM node:20.15.1-alpine AS app-shell-deps +RUN apk add --no-cache git +WORKDIR /app/app-shell +COPY app-shell/package.json app-shell/yarn.lock ./ +RUN yarn install --pure-lockfile + +# Nginx setup and application build +FROM node:20.15.1-alpine AS build +RUN apk add --no-cache git nginx +RUN apk add --no-cache lsof procps +RUN yarn global add concurrently + +RUN mkdir -p /app/pids + +# Make sure to add yarn global bin to PATH +ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:$PATH + +# Copy dependencies +WORKDIR /app +COPY --from=frontend-deps /app/frontend /app/frontend +COPY --from=backend-deps /app/backend /app/backend +COPY --from=app-shell-deps /app/app-shell /app/app-shell + +COPY frontend /app/frontend +COPY backend /app/backend +COPY app-shell /app/app-shell +COPY docker /app/docker + +# Copy Nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy custom error page +COPY 502.html /usr/share/nginx/html/502.html + +# Change owner and permissions of the error page +RUN chown nginx:nginx /usr/share/nginx/html/502.html && \ + chmod 644 /usr/share/nginx/html/502.html + +# Copy all files from root to /app +COPY . /app + +# Expose the port the app runs on +EXPOSE 8080 +ENV NODE_ENV=dev_stage +ENV FRONT_PORT=3001 +ENV BACKEND_PORT=3000 +ENV APP_SHELL_PORT=4000 + +CMD ["sh", "-c", "\ + yarn --cwd /app/frontend dev & echo $! > /app/pids/frontend.pid && \ + yarn --cwd /app/backend start & echo $! > /app/pids/backend.pid && \ + sleep 10 && nginx -g 'daemon off;' & \ + NGINX_PID=$! && \ + echo 'Waiting for backend (port 3000) to be available...' && \ + while ! nc -z localhost ${BACKEND_PORT}; do \ + sleep 2; \ + done && \ + echo 'Backend is up. Starting app_shell for Git check...' && \ + yarn --cwd /app/app-shell start && \ + wait $NGINX_PID"] \ No newline at end of file diff --git a/LICENSE.MD b/LICENSE.MD new file mode 100644 index 0000000..a2b571e --- /dev/null +++ b/LICENSE.MD @@ -0,0 +1,74 @@ +Flatlogic Community Licence 1.0.0 +--------------------------------- +Required Notice: Copyright © 2025 Flatlogic sp. z o.o. (https://flatlogic.com) + +## Acceptance +In order to get any licence under these terms, you must agree to them as both strict obligations and conditions to all your licences. + +## Copyright Licence +The licensor grants you a copyright licence for the software to do everything you might do with the software that would otherwise infringe the licensor’s copyright in it for any permitted purpose. However, you may only distribute the software according to **Distribution Licence** and make changes or new works based on the software according to **Changes and New Works Licence**. + +## Distribution Licence +The licensor grants you an additional copyright licence to distribute copies of the software. Your licence to distribute covers distributing the software with changes and new works permitted by **Changes and New Works Licence**. + +## Notices +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain‑text lines beginning with `Required Notice:` that the licensor provided with the software. For example: + +> Required Notice: Copyright © 2025 Flatlogic sp. z o.o. (https://flatlogic.com) + +## Changes and New Works Licence +The licensor grants you an additional copyright licence to make changes and new works based on the software for any permitted purpose. + +## Patent Licence +The licensor grants you a patent licence for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. + +## Noncompete +Any purpose is a permitted purpose, **except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.** + +## Competition +Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they’re written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. + +If you market a product as a practical substitute for the software or another product, it definitely competes. + +## New Products +If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions. + +## Discontinued Products +You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain‑text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example: + +> Licensor Line of Business: Flatlogic Generator SaaS (https://flatlogic.com/generator) + +## Sales of Business +If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce **Noncompete** for that product. + +## Fair Use +You may have “fair use” rights for the software under the law. These terms do not limit them. + +## No Other Rights +These terms do not allow you to sublicense or transfer any of your licences to anyone else, or prevent the licensor from granting licences to anyone else. These terms do not imply any other licences. + +## Patent Defence +If you make any written claim that the software infringes or contributes to infringement of any patent, your patent licence for the software granted under these terms ends immediately. If your company makes such a claim, your patent licence ends immediately for work on behalf of your company. + +## Violations +The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licences, your licences can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licences end immediately. + +## No Liability +As far as the law allows, the software comes as‑is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. + +## Definitions +*The licensor* is Flatlogic sp. z o.o., and *the software* is the **Flatlogic Community Template** we make available under these terms. + +*A product* can be a good or service, or a combination of them. + +*You* refers to the individual or entity agreeing to these terms. + +*Your company* is any legal entity, sole proprietorship, or other kind of organisation that you work for, plus all its affiliates. + +*Affiliates* means the other organisations that an organisation has control over, is under the control of, or is under common control with. + +*Control* means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +*Your licences* are all the licences granted to you for the software under these terms. + +*Use* means anything you do with the software requiring one of your licences. diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..8655a0d --- /dev/null +++ b/README.MD @@ -0,0 +1,170 @@ +# Project Setup & Local Development + +## Tech Stack + +| Layer | Technology | +| --------- | -------------------- | +| Front‑end | **React JS** | +| Back‑end | **Node JS** | +| Database | **PostgreSQL** | +| Container | **Docker & Compose** | + +--- + +## 1 — Run Locally (without Docker) + +### 1.1 Backend + +```bash +cd backend +# install dependencies +yarn install +``` + +\#### Configure PostgreSQL + +
+macOS + +```bash +brew install postgres +``` + +
+
+Ubuntu + +```bash +sudo apt update +sudo apt install postgresql postgresql-contrib +``` + +
+ +\##### Create DB & Admin User + +```bash +# log in as default super‑user +psql postgres -U postgres + +-- inside psql +CREATE ROLE admin WITH LOGIN PASSWORD 'admin_pass'; +ALTER ROLE admin CREATEDB; +\q + +# log in as the new user +psql postgres -U admin + +-- inside psql +CREATE DATABASE db_; +GRANT ALL PRIVILEGES ON DATABASE db_ TO admin; +\q +``` + +\##### Migrate & Start + +```bash +yarn db:create # generate schema +yarn start # production build +``` + +### 1.2 Frontend + +```bash +cd frontend +yarn install +yarn start +``` + +> Front‑end dev‑server runs at **[http://localhost:3000](http://localhost:3000)** by default. + +--- + +## 2 — Run with Docker + +```bash +cd docker +chmod +x wait-for-it.sh start-backend.sh +``` + +| Scenario | Command | +| ------------------------- | ---------------------------------- | +| **Fresh DB volume** | `rm -rf data && docker-compose up` | +| **Reuse existing volume** | `docker-compose up` | + +Then open **[http://localhost:3000](http://localhost:3000)**. + +Stop services with **Ctrl + C** or: + +```bash +docker-compose down +``` + +> **Heads‑up:** Files inside the `docker/` folder and the root `Dockerfile` are used for cloud deployment. Changing them may break the pipeline. + +--- + +## Folder Structure (top‑level) + +```text +├── backend/ # Node JS API & services +├── frontend/ # React application +├── docker/ # Compose files & helper scripts +└── README.md # (this file) +``` + +--- + +## Troubleshooting + +### “connection refused” + +1. Port closed or backlog full. +2. Firewall (local or network). +3. Service not running. + +Verify with: + +```bash +telnet +``` + +### macOS + +```bash +sudo service ssh status +``` + +### Ubuntu – IP conflict check + +```bash +arp-scan -I eth0 -l | grep +arping +``` + +### Reset PostgreSQL schema (macOS example) + +```sql +DROP SCHEMA public CASCADE; +CREATE SCHEMA public; +GRANT ALL ON SCHEMA public TO postgres; +GRANT ALL ON SCHEMA public TO public; +``` + +--- + +## Cheat‑sheet + +| Task | Command | +| ----------------------- | ---------------------------------- | +| **Create DB schema** | `yarn db:create` | +| **Start backend** | `yarn start` | +| **Start dev front‑end** | `cd frontend && yarn start` | +| **Compose up (fresh)** | `rm -rf data && docker-compose up` | +| **Compose down** | `docker-compose down` | + +
+ +--- + +Made with ❤️ by Flatlogic Platform. diff --git a/app-shell/.eslintrc.cjs b/app-shell/.eslintrc.cjs new file mode 100644 index 0000000..563d159 --- /dev/null +++ b/app-shell/.eslintrc.cjs @@ -0,0 +1,26 @@ +const globals = require('globals'); + +module.exports = [ + { + files: ['**/*.js', '**/*.ts', '**/*.tsx'], + languageOptions: { + ecmaVersion: 2021, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + }, + parser: '@typescript-eslint/parser', + }, + plugins: ['@typescript-eslint'], + rules: { + 'no-unused-vars': 'warn', + 'no-console': 'off', + 'indent': ['error', 2], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + + '@typescript-eslint/no-unused-vars': 'warn', + }, + }, +]; \ No newline at end of file diff --git a/app-shell/.prettierrc b/app-shell/.prettierrc new file mode 100644 index 0000000..bb087f2 --- /dev/null +++ b/app-shell/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "printWidth": 80, + "trailingComma": "all", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always" +} diff --git a/app-shell/.sequelizerc b/app-shell/.sequelizerc new file mode 100644 index 0000000..fe89188 --- /dev/null +++ b/app-shell/.sequelizerc @@ -0,0 +1,7 @@ +const path = require('path'); +module.exports = { + "config": path.resolve("src", "db", "db.config.js"), + "models-path": path.resolve("src", "db", "models"), + "seeders-path": path.resolve("src", "db", "seeders"), + "migrations-path": path.resolve("src", "db", "migrations") +}; \ No newline at end of file diff --git a/app-shell/Dockerfile b/app-shell/Dockerfile new file mode 100644 index 0000000..eb79c5d --- /dev/null +++ b/app-shell/Dockerfile @@ -0,0 +1,23 @@ +FROM node:20.15.1-alpine + +RUN apk update && apk add bash +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + + +# Bundle app source +COPY . . + + +EXPOSE 4000 + +CMD [ "yarn", "start" ] diff --git a/app-shell/README.md b/app-shell/README.md new file mode 100644 index 0000000..c53191f --- /dev/null +++ b/app-shell/README.md @@ -0,0 +1,13 @@ +#test - template backend, + +#### Run App on local machine: + +##### Install local dependencies: + +- `yarn install` + +--- + +##### Start build: + +- `yarn start` diff --git a/app-shell/package.json b/app-shell/package.json new file mode 100644 index 0000000..e33f634 --- /dev/null +++ b/app-shell/package.json @@ -0,0 +1,42 @@ +{ + "name": "app-shell", + "description": "app-shell", + "scripts": { + "start": "node ./src/index.js" + }, + "dependencies": { + "@babel/parser": "^7.26.7", + "adm-zip": "^0.5.16", + "axios": "^1.6.7", + "bcrypt": "5.1.1", + "cors": "2.8.5", + "eslint": "^9.13.0", + "express": "4.18.2", + "formidable": "1.2.2", + "helmet": "4.1.1", + "json2csv": "^5.0.7", + "jsonwebtoken": "8.5.1", + "lodash": "4.17.21", + "moment": "2.30.1", + "multer": "^1.4.4", + "passport": "^0.7.0", + "passport-google-oauth2": "^0.2.0", + "passport-jwt": "^4.0.1", + "passport-microsoft": "^0.1.0", + "postcss": "^8.5.1", + "sequelize-json-schema": "^2.1.1", + "pg": "^8.13.3" + }, + "engines": { + "node": ">=18" + }, + "private": true, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.12.2", + "@typescript-eslint/parser": "^8.12.2", + "cross-env": "7.0.3", + "mocha": "8.1.3", + "nodemon": "^3.1.7", + "sequelize-cli": "6.6.2" + } +} diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json new file mode 100644 index 0000000..23958e6 --- /dev/null +++ b/app-shell/src/_schema.json @@ -0,0 +1,5 @@ + + +{ + "Initial version": "{\"iv\":\"+bHdVruoSLOkzWB+\",\"encryptedData\":\"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\"}" +} diff --git a/app-shell/src/config.js b/app-shell/src/config.js new file mode 100644 index 0000000..fe12d15 --- /dev/null +++ b/app-shell/src/config.js @@ -0,0 +1,16 @@ + + +const config = { + schema_encryption_key: process.env.SCHEMA_ENCRYPTION_KEY || '', + + project_uuid: 'f087aa9d-30b0-40b5-857b-8f5c99381bbf', + flHost: process.env.NODE_ENV === 'production' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects', + + gitea_domain: process.env.GITEA_DOMAIN || 'gitea.flatlogic.app', + gitea_username: process.env.GITEA_USERNAME || 'admin', + gitea_api_token: process.env.GITEA_API_TOKEN || null, + github_repo_url: process.env.GITHUB_REPO_URL || null, + github_token: process.env.GITHUB_TOKEN || null, +}; + +module.exports = config; diff --git a/app-shell/src/helpers.js b/app-shell/src/helpers.js new file mode 100644 index 0000000..1d918b5 --- /dev/null +++ b/app-shell/src/helpers.js @@ -0,0 +1,23 @@ +const jwt = require('jsonwebtoken'); +const config = require('./config'); + +module.exports = class Helpers { + static wrapAsync(fn) { + return function (req, res, next) { + fn(req, res, next).catch(next); + }; + } + + static commonErrorHandler(error, req, res, next) { + if ([400, 403, 404].includes(error.code)) { + return res.status(error.code).send(error.message); + } + + console.error(error); + return res.status(500).send(error.message); + } + + static jwtSign(data) { + return jwt.sign(data, config.secret_key, { expiresIn: '6h' }); + } +}; diff --git a/app-shell/src/index.js b/app-shell/src/index.js new file mode 100644 index 0000000..496296e --- /dev/null +++ b/app-shell/src/index.js @@ -0,0 +1,54 @@ +const express = require('express'); +const cors = require('cors'); +const app = express(); +const bodyParser = require('body-parser'); +const checkPermissions = require('./middlewares/check-permissions'); +const modifyPath = require('./middlewares/modify-path'); +const VCS = require('./services/vcs'); + +const executorRoutes = require('./routes/executor'); +const vcsRoutes = require('./routes/vcs'); + +// Function to initialize the Git repository +function initRepo() { + const projectId = '32693'; + return VCS.initRepo(projectId); +} + +// Start the Express app on APP_SHELL_PORT (4000) +function startServer() { + const PORT = 4000; + app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`); + }); +} + +// Run Git check after the server is up +function runGitCheck() { + initRepo() + .then(result => { + console.log(result?.message ? result.message : result); + // Here you can add additional logic if needed + }) + .catch(err => { + console.error('Error during repo initialization:', err); + // Optionally exit the process if Git check is critical: + // process.exit(1); + }); +} + +app.use(cors({ origin: true })); +app.use(bodyParser.json()); +app.use(checkPermissions); +app.use(modifyPath); + +app.use('/executor', executorRoutes); +app.use('/vcs', vcsRoutes); + +// Start the app_shell server +startServer(); + +// Now perform Git check +runGitCheck(); + +module.exports = app; diff --git a/app-shell/src/middlewares/check-permissions.js b/app-shell/src/middlewares/check-permissions.js new file mode 100644 index 0000000..cc9d90a --- /dev/null +++ b/app-shell/src/middlewares/check-permissions.js @@ -0,0 +1,17 @@ +const config = require('../config'); + +function checkPermissions(req, res, next) { + const project_uuid = config.project_uuid; + const requiredHeader = 'X-Project-UUID'; + const headerValue = req.headers[requiredHeader.toLowerCase()]; + // Logging whatever request we're getting + console.log('Request:', req.url, req.method, req.body, req.headers); + + if (headerValue && headerValue === project_uuid) { + next(); + } else { + res.status(403).send({ error: 'Stop right there, criminal scum! Your project UUID is invalid or missing.' }); + } +} + +module.exports = checkPermissions; \ No newline at end of file diff --git a/app-shell/src/middlewares/modify-path.js b/app-shell/src/middlewares/modify-path.js new file mode 100644 index 0000000..0154280 --- /dev/null +++ b/app-shell/src/middlewares/modify-path.js @@ -0,0 +1,8 @@ +function modifyPath(req, res, next) { + if (req.body && req.body.path) { + req.body.path = '../../../' + req.body.path; + } + next(); + } + +module.exports = modifyPath; \ No newline at end of file diff --git a/app-shell/src/routes/executor.js b/app-shell/src/routes/executor.js new file mode 100644 index 0000000..588cfff --- /dev/null +++ b/app-shell/src/routes/executor.js @@ -0,0 +1,312 @@ +const express = require('express'); +const multer = require('multer'); +const upload = multer({ dest: 'uploads/' }); +const fs = require('fs'); + +const ExecutorService = require('../services/executor'); + +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +router.post( + '/read_project_tree', + wrapAsync(async (req, res) => { + const { path } = req.body; + const tree = await ExecutorService.readProjectTree(path); + res.status(200).send(tree); + }), +); + +router.post( + '/read_file', + wrapAsync(async (req, res) => { + const { path, showLines } = req.body; + const content = await ExecutorService.readFileContents(path, showLines); + res.status(200).send(content); + }), +); + +router.post( + '/count_file_lines', + wrapAsync(async (req, res) => { + const { path } = req.body; + const content = await ExecutorService.countFileLines(path); + res.status(200).send(content); + }), +); + +// router.post( +// '/read_file_header', +// wrapAsync(async (req, res) => { +// const { path, N } = req.body; +// try { +// const header = await ExecutorService.readFileHeader(path, N); +// res.status(200).send(header); +// } catch (error) { +// res.status(500).send({ +// error: true, +// message: error.message, +// details: error.details || error.stack, +// validation: error.validation +// }); +// } +// }), +// ); + +router.post( + '/read_file_line_context', + wrapAsync(async (req, res) => { + const { path, lineNumber, windowSize, showLines } = req.body; + try { + const context = await ExecutorService.readFileLineContext(path, lineNumber, windowSize, showLines); + res.status(200).send(context); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/write_file', + wrapAsync(async (req, res) => { + const { path, fileContents, comment } = req.body; + try { + await ExecutorService.writeFile(path, fileContents, comment); + res.status(200).send({ message: 'File written successfully' }); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/insert_file_content', + wrapAsync(async (req, res) => { + const { path, lineNumber, newContent, message } = req.body; + try { + await ExecutorService.insertFileContent(path, lineNumber, newContent, message); + res.status(200).send({ message: 'File written successfully' }); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/replace_file_line', + wrapAsync(async (req, res) => { + const { path, lineNumber, newText } = req.body; + try { + const result = await ExecutorService.replaceFileLine(path, lineNumber, newText); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); +router.post( + '/replace_file_chunk', + wrapAsync(async (req, res) => { + const { path, startLine, endLine, newCode } = req.body; + try { + const result = await ExecutorService.replaceFileChunk(path, startLine, endLine, newCode); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/delete_file_lines', + wrapAsync(async (req, res) => { + const { path, startLine, endLine, message } = req.body; + try { + const result = await ExecutorService.deleteFileLines(path, startLine, endLine, message); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/validate_file', + wrapAsync(async (req, res) => { + const { path } = req.body; + try { + const validationResult = await ExecutorService.validateFile(path); + res.status(200).send({ validationResult }); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + + +router.post( + '/check_frontend_runtime_error', + wrapAsync(async (req, res) => { + try { + const result = await ExecutorService.checkFrontendRuntimeLogs(); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ error: error }); + } + }), +); + + +router.post( + '/replace_code_block', + wrapAsync(async (req, res) => { + const {path, oldCode, newCode, message} = req.body; + try { + const response = await ExecutorService.replaceCodeBlock(path, oldCode, newCode, message); + res.status(200).send(response); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }) + } + }) +) + +router.post('/update_project_files_from_scheme', + upload.single('file'), // 'file' - name of the field in the form + async (req, res) => { + console.log('Request received'); + console.log('Headers:', req.headers); + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded' }); + } + + console.log('File info:', { + originalname: req.file.originalname, + path: req.file.path, + size: req.file.size, + mimetype: req.file.mimetype + }); + + try { + console.log('Starting update process...'); + const result = await ExecutorService.updateProjectFilesFromScheme(req.file.path); + console.log('Update completed, result:', result); + + console.log('Removing temp file...'); + fs.unlinkSync(req.file.path); + console.log('Temp file removed'); + + console.log('Sending response...'); + return res.json(result); + } catch (error) { + console.error('Error in route handler:', error); + if (req.file) { + try { + fs.unlinkSync(req.file.path); + console.log('Temp file removed after error'); + } catch (unlinkError) { + console.error('Error removing temp file:', unlinkError); + } + } + console.error('Update project files error:', error); + return res.status(500).json({ + error: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); + } + } +); + +router.post( + '/get_db_schema', + wrapAsync(async (req, res) => { + try { + + const jsonSchema = await ExecutorService.getDBSchema(); + res.status(200).send({ jsonSchema }); + } catch (error) { + res.status(500).send({ error: error }); + } + }), +); + +router.post( + '/execute_sql', + wrapAsync(async (req, res) => { + try { + const { query } = req.body; + const result = await ExecutorService.executeSQL(query); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ error: error }); + } + }), +); + +router.post( + '/search_files', + wrapAsync(async (req, res) => { + try { + const { searchStrings } = req.body; + + if ( + typeof searchStrings !== 'string' && + !( + Array.isArray(searchStrings) && + searchStrings.every(item => typeof item === 'string') + ) + ) { + return res.status(400).send({ error: 'searchStrings must be a string or an array of strings' }); + } + + const result = await ExecutorService.searchFiles(searchStrings); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ error: error.message }); + } + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/app-shell/src/routes/vcs.js b/app-shell/src/routes/vcs.js new file mode 100644 index 0000000..926498d --- /dev/null +++ b/app-shell/src/routes/vcs.js @@ -0,0 +1,40 @@ +const express = require('express'); +const wrapAsync = require('../helpers').wrapAsync; // Ваша обёртка для обработки асинхронных маршрутов +const VSC = require('../services/vcs'); +const router = express.Router(); + +router.post('/init', wrapAsync(async (req, res) => { + const result = await VSC.initRepo(); + res.status(200).send(result); +})); + +router.post('/commit', wrapAsync(async (req, res) => { + const { message, files, dev_schema } = req.body; + const result = await VSC.commitChanges(message, files, dev_schema); + res.status(200).send(result); +})); + +router.post('/log', wrapAsync(async (req, res) => { + const result = await VSC.getLog(); + res.status(200).send(result); +})); + +router.post('/rollback', wrapAsync(async (req, res) => { + const { ref } = req.body; + // const result = await VSC.checkout(ref); + const result = await VSC.revert(ref); + res.status(200).send(result); +})); + +router.post('/sync-to-stable', wrapAsync(async (req, res) => { + const result = await VSC.mergeDevIntoMaster(); + res.status(200).send(result); +})); + +router.post('/reset-dev', wrapAsync(async (req, res) => { + const result = await VSC.resetDevBranch(); + res.status(200).send(result); +})); + +router.use('/', require('../helpers').commonErrorHandler); +module.exports = router; \ No newline at end of file diff --git a/app-shell/src/services/database.js b/app-shell/src/services/database.js new file mode 100644 index 0000000..bf8f3a9 --- /dev/null +++ b/app-shell/src/services/database.js @@ -0,0 +1,88 @@ +// Database.js +const { Client } = require('pg'); +const config = require('../../../backend/src/db/db.config'); + +const env = process.env.NODE_ENV || 'development'; +const dbConfig = config[env]; + +class Database { + constructor() { + this.client = new Client({ + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, + host: dbConfig.host, + port: dbConfig.port + }); + + // Connect once, reuse the client + this.client.connect().catch(err => { + console.error('Error connecting to the database:', err); + throw err; + }); + } + + async executeSQL(query) { + try { + const result = await this.client.query(query); + return { + success: true, + rows: result.rows + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + // Method to fetch simple table/column info from 'information_schema' + // (You can expand this to handle constraints, indexes, etc.) + async getDBSchema(schemaName = 'public') { + try { + const tableQuery = ` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = $1 + AND table_type = 'BASE TABLE' + ORDER BY table_name + `; + + const columnQuery = ` + SELECT table_name, column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_schema = $1 + ORDER BY table_name, ordinal_position + `; + + const [tablesResult, columnsResult] = await Promise.all([ + this.client.query(tableQuery, [schemaName]), + this.client.query(columnQuery, [schemaName]), + ]); + + // Build a simple schema object: + const tables = tablesResult.rows.map(row => row.table_name); + const columnsByTable = {}; + + columnsResult.rows.forEach(row => { + const { table_name, column_name, data_type, is_nullable } = row; + if (!columnsByTable[table_name]) columnsByTable[table_name] = []; + columnsByTable[table_name].push({ column_name, data_type, is_nullable }); + }); + + // Combine tables with their columns + return tables.map(table => ({ + table, + columns: columnsByTable[table] || [], + })); + } catch (error) { + console.error('Error fetching schema:', error); + throw error; + } + } + + async close() { + await this.client.end(); + } +} + +module.exports = new Database(); diff --git a/app-shell/src/services/executor.js b/app-shell/src/services/executor.js new file mode 100644 index 0000000..eecb869 --- /dev/null +++ b/app-shell/src/services/executor.js @@ -0,0 +1,1206 @@ +const fs = require('fs').promises; +const os = require('os'); +const path = require('path'); +const AdmZip = require('adm-zip'); +const { exec } = require('child_process'); +const util = require('util'); +const ProjectEventsService = require('./project-events'); +const config = require('../config.js'); +// Babel Parser for JS/TS/TSX +const babelParser = require('@babel/parser'); +const babelParse = babelParser.parse; + +// Local App DB Connection +const database = require('./database'); + +// PostCSS for CSS +const postcss = require('postcss'); + +const execAsync = util.promisify(exec); + +module.exports = class ExecutorService { + static async readProjectTree(directoryPath) { + const paths = { + frontend: '../../../frontend', + backend: '../../../backend', + default: '../../../' + }; + + try { + const publicDir = path.join(__dirname, paths[directoryPath] || directoryPath || paths.default); + + return await getDirectoryTree(publicDir); + } catch (error) { + console.error('Error reading directory:', error); + + throw error; + } + } + + static async readFileContents(filePath, showLines) { + try { + const fullPath = path.join(__dirname, filePath); + const content = await fs.readFile(fullPath, 'utf8'); + + if (showLines) { + const lines = content.split('\n'); + + const lineObject = {}; + lines.forEach((line, index) => { + lineObject[index + 1] = line; + }); + + return lineObject; + } else { + return content; + } + } catch (error) { + console.error('Error reading file:', error); + throw error; + } + } + + static async countFileLines(filePath) { + try { + const fullPath = path.join(__dirname, filePath); + + // Check file exists + await fs.access(fullPath); + + // Read file content + const content = await fs.readFile(fullPath, 'utf8'); + + // Split by newline and count + const lines = content.split('\n'); + + return { + success: true, + lineCount: lines.length + }; + } catch (error) { + console.error('Error counting file lines:', error); + return { + success: false, + message: error.message + }; + } + } + + // static async readFileHeader(filePath, N = 30) { + // try { + // const fullPath = path.join(__dirname, filePath); + // const content = await fs.readFile(fullPath, 'utf8'); + // const lines = content.split('\n'); + // + // if (lines.length < N) { + // return { error: `File has less than ${N} lines` }; + // } + // + // const headerLines = lines.slice(0, Math.min(50, lines.length)); + // + // const lineObject = {}; + // headerLines.forEach((line, index) => { + // lineObject[index + 1] = line; + // }); + // + // return lineObject; + // } catch (error) { + // console.error('Error reading file header:', error); + // throw error; + // } + // } + + static async readFileLineContext(filePath, lineNumber, windowSize, showLines) { + try { + const fullPath = path.join(__dirname, filePath); + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + const start = Math.max(0, lineNumber - windowSize); + const end = Math.min(lines.length, lineNumber + windowSize + 1); + + const contextLines = lines.slice(start, end); + + if (showLines) { + const lineObject = {}; + contextLines.forEach((line, index) => { + lineObject[start + index + 1] = line; + }); + + return lineObject; + } else { + return contextLines.join('\n'); + } + } catch (error) { + console.error('Error reading file line context:', error); + throw error; + } + } + + static async validateFile(filePath) { + console.log('Validating file:', filePath); + + // Read file content + let content; + try { + content = await fs.readFile(filePath, 'utf8'); + } catch (err) { + throw new Error(`Could not read file: ${filePath}\n${err.message}`); + } + + // Determine file extension + let ext = path.extname(filePath).toLowerCase(); + if (ext === '.temp') { + ext = path.extname(filePath.slice(0, -5)).toLowerCase(); + } + + try { + switch (ext) { + case '.js': + case '.ts': + case '.tsx': { + // Parse JS/TS/TSX with Babel + babelParse(content, { + sourceType: 'module', + // plugins array covers JS, TS, TSX, and optional JS flavors + plugins: ['jsx', 'typescript'] + }); + break; + } + + case '.css': { + // Parse CSS with PostCSS + postcss.parse(content); + break; + } + + default: { + // If the extension isn't recognized, assume it's "valid" + // or you could throw an error to force a known extension + console.warn(`No validation implemented for extension "${ext}". Skipping syntax check.`); + } + } + + // If parsing succeeded, return true + return true; + + } catch (parseError) { + // Rethrow parse errors with a friendlier message + throw parseError; + } + } + + static async checkFrontendRuntimeLogs() { + const frontendLogPath = '../frontend/json/runtimeError.json'; + + try { + // Check if file exists + try { + console.log('Accessing frontend logs:', frontendLogPath); + await fs.access(frontendLogPath); + } catch (error) { + console.log('Frontend logs not found:', error); + // File doesn't exist - return empty object + return {runtime_error: {}}; + } + + // File exists, try to read it + try { + // Read the entire file instead of using tail + const fileContent = await fs.readFile(frontendLogPath, 'utf8'); + console.log('Reading frontend logs:', fileContent); + + // Handle empty file + if (!fileContent || fileContent.trim() === '') { + return {runtime_error: {}}; + } + + // Parse JSON content + const runtime_error = JSON.parse(fileContent); + + console.log('Parsed frontend logs:', runtime_error); + return {runtime_error}; + } catch (error) { + // Error reading or parsing file + console.error('Error reading frontend runtime logs:', error); + return {runtime_error: {}}; + } + } catch (error) { + // Unexpected error + console.log('Error checking frontend logs:', error); + return {runtime_error: {}}; + } + } + + static async writeFile(filePath, fileContents, comment) { + try { + console.log(comment) + const fullPath = path.join(__dirname, filePath); + + // Write to a temp file first + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, fileContents, 'utf8'); + + // Validate the temp file + await this.validateFile(tempPath); + + // Rename temp file to original path + await fs.rename(tempPath, fullPath); + + return true; + } catch (error) { + console.error('Error writing file:', error); + throw error; + } + } + + static async insertFileContent(filePath, lineNumber, newContent, message) { + try { + const fullPath = path.join(__dirname, filePath); + + // Check file exists + await fs.access(fullPath); + + // Read and split by line + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + // Ensure lineNumber is within [1 ... lines.length + 1] + // 1 means "insert at the very first line" + // lines.length + 1 means "append at the end" + if (lineNumber < 1) { + lineNumber = 1; + } + if (lineNumber > lines.length + 1) { + lineNumber = lines.length + 1; + } + + // Convert to 0-based index + const insertIndex = lineNumber - 1; + + // Prepare preview + const preview = { + insertionLine: lineNumber, + insertedLines: newContent.split('\n') + }; + + // Insert newContent lines at the specified index + lines.splice(insertIndex, 0, ...newContent.split('\n')); + + // Write changes to a temp file first + const updatedContent = lines.join('\n'); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, updatedContent, 'utf8'); + + await this.validateFile(tempPath); + + // Rename temp file to original path + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error inserting file content:', error); + throw error; + } + } + + static async replaceFileLine(filePath, lineNumber, newText, message = null) { + const fullPath = path.join(__dirname, filePath); + try { + + try { + await fs.access(fullPath); + } catch (error) { + throw new Error(`File not found: ${filePath}`); + } + + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + if (lineNumber < 1 || lineNumber > lines.length) { + throw new Error(`Invalid line number: ${lineNumber}. File has ${lines.length} lines`); + } + + if (typeof newText !== 'string') { + throw new Error('New text must be a string'); + } + + const preview = { + oldLine: lines[lineNumber - 1], + newLine: newText, + lineNumber: lineNumber + }; + + lines[lineNumber - 1] = newText; + const newContent = lines.join('\n'); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, newContent, 'utf8'); + + await this.validateFile(tempPath); + + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error updating file line:', error); + + try { + await fs.unlink(`${fullPath}.temp`); + } catch { + } + + throw { + error: error, + message: error.message, + details: error.stack + }; + } + } + + static async replaceFileChunk(filePath, startLine, endLine, newCode) { + try { + // Check if this is a single-line change + const newCodeLines = newCode.split('\n'); + if (newCodeLines.length === 1 && endLine === startLine) { + // Redirect to replace_file_line + return await this.replaceFileLine(filePath, startLine, newCode); + } + + const fullPath = path.join(__dirname, filePath); + + // Check if file exists + try { + await fs.access(fullPath); + } catch (error) { + throw new Error(`File not found: ${filePath}`); + } + + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + // Adjust line numbers to array indices (subtract 1) + const startIndex = startLine - 1; + const endIndex = endLine - 1; + + // Validate input parameters + if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) { + throw new Error(`Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines`); + } + + // Check type of new code + if (typeof newCode !== 'string') { + throw new Error('New code must be a string'); + } + + // Create changes preview + const preview = { + oldLines: lines.slice(startIndex, endIndex + 1), + newLines: newCode.split('\n'), + startLine, + endLine + }; + + // Apply changes to temp file first + lines.splice(startIndex, endIndex - startIndex + 1, ...newCode.split('\n')); + const newContent = lines.join(os.EOL); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, newContent, 'utf8'); + await this.validateFile(tempPath); + // Apply changes if all validations passed + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error updating file slice:', error); + + // Clean up temp file if exists + try { + await fs.unlink(`${fullPath}.temp`); + } catch { + } + + throw { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async replaceCodeBlock(filePath, oldCode, newCode, message) { + try { + console.log(message); + const fullPath = path.join(__dirname, filePath); + + // Check file exists + await fs.access(fullPath); + + // Read file content + let content = await fs.readFile(fullPath, 'utf8'); + + // A small helper to unify line breaks to just `\n` + const unifyLineBreaks = (str) => str.replace(/\r\n/g, '\n'); + + // Normalize line breaks in file content, oldCode, and newCode + content = unifyLineBreaks(content); + oldCode = unifyLineBreaks(oldCode); + newCode = unifyLineBreaks(newCode); + + // Optional: Trim trailing spaces or handle other whitespace normalization if needed + // oldCode = oldCode.trim(); + // newCode = newCode.trim(); + + // Check if oldCode actually exists in the content + const index = content.indexOf(oldCode); + if (index === -1) { + return { + success: false, + message: 'Old code not found in file.' + }; + } + + // Create a preview before replacing + const preview = { + oldCodeSnippet: oldCode, + newCodeSnippet: newCode + }; + + // Perform replacement (single occurrence). For multiple, use replaceAll or a loop. + // If you want a global replacement, consider: + // content = content.split(oldCode).join(newCode); + content = content.replace(oldCode, newCode); + + // Write to a temp file first + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, content, 'utf8'); + + await this.validateFile(tempPath); + // Rename temp file to original + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error replacing code:', error); + return { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + //todo add validation + static async deleteFileLines(filePath, startLine, endLine, veryShortDescription) { + try { + const fullPath = path.join(__dirname, filePath); + + // Check if file exists + await fs.access(fullPath); + + // Read file content + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + // Convert to zero-based indices + const startIndex = startLine - 1; + const endIndex = endLine - 1; + + // Validate range + if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) { + throw new Error( + `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines` + ); + } + + // Prepare a preview of the lines being deleted + const preview = { + deletedLines: lines.slice(startIndex, endIndex + 1), + startLine, + endLine + }; + + // Remove lines + lines.splice(startIndex, endIndex - startIndex + 1); + + // Join remaining lines and write to a temporary file + const newContent = lines.join('\n'); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, newContent, 'utf8'); + + await this.validateFile(tempPath); + // Rename temp file to original + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error deleting file lines:', error); + return { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async validateTypeScript(filePath, content = null) { + try { + // Basic validation of JSX syntax + const jsxErrors = []; + + if (content !== null) { + // Check for matching braces + if ((content.match(/{/g) || []).length !== (content.match(/}/g) || []).length) { + jsxErrors.push("Unmatched curly braces"); + } + + // Check for invalid syntax in JSX attributes + if (content.includes('label={')) { + if (!content.match(/label={[^}]+}/)) { + jsxErrors.push("Invalid label attribute syntax"); + } + } + + if (jsxErrors.length > 0) { + return { + valid: false, + errors: jsxErrors.map(error => ({ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: error + })) + }; + } + } + + return { + valid: true, + errors: [], + errorCount: 0, + warningCount: 0 + }; + + } catch (error) { + console.error('TypeScript validation error:', error); + return { + valid: false, + errors: [{ + code: 'VALIDATION_FAILED', + severity: 'error', + location: '', + message: `TypeScript validation error: ${error.message}` + }], + errorCount: 1, + warningCount: 0 + }; + } + } + + static async validateBackendFiles(backendPath) { + try { + // Check for syntax errors + await execAsync(`node --check ${backendPath}/src/index.js`); + + // Try to run the code in a test environment + const testProcess = exec( + 'NODE_ENV=test node -e "try { require(\'./src/index.js\') } catch(e) { console.error(e); process.exit(1) }"', + {cwd: backendPath} + ); + + return new Promise((resolve) => { + let output = ''; + let error = ''; + + testProcess.stdout.on('data', (data) => { + output += data; + }); + + testProcess.stderr.on('data', (data) => { + error += data; + }); + + testProcess.on('close', (code) => { + if (code === 0) { + resolve({valid: true}); + } else { + resolve({ + valid: false, + error: error || output + }); + } + }); + + // Timeout on validation + setTimeout(() => { + testProcess.kill(); + resolve({ + valid: true, + warning: 'Validation timeout, but no immediate errors found' + }); + }, 5000); + }); + } catch (error) { + return { + valid: false, + error: error.message + }; + } + } + + static async createBackup(ROOT_PATH) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupDir = path.join(ROOT_PATH, 'backups', timestamp); + + try { + await fs.mkdir(path.join(ROOT_PATH, 'backups'), {recursive: true}); + + const dirsToBackup = ['frontend', 'backend']; + + for (const dir of dirsToBackup) { + const sourceDir = path.join(ROOT_PATH, dir); + const targetDir = path.join(backupDir, dir); + + await fs.mkdir(targetDir, {recursive: true}); + + await execAsync( + `cd "${sourceDir}" && ` + + `find . -type f -not -path "*/node_modules/*" -not -path "*/\\.*" | ` + + `while read file; do ` + + `mkdir -p "${targetDir}/$(dirname "$file")" && ` + + `cp "$file" "${targetDir}/$file"; ` + + `done` + ); + } + + console.log('Backup created at:', backupDir); + return backupDir; + } catch (error) { + console.error('Error creating backup:', error); + throw error; + } + } + + static async restoreFromBackup(backupDir, ROOT_PATH) { + try { + console.log('Restoring from backup:', backupDir); + await execAsync(`rm -rf ${ROOT_PATH}/backend/*`); + await execAsync(`cp -r ${backupDir}/* ${ROOT_PATH}/backend/`); + return true; + } catch (error) { + console.error('Error restoring from backup:', error); + throw error; + } + } + + static async updateProjectFilesFromScheme(zipFilePath) { + const MAX_FILE_SIZE = 10 * 1024 * 1024; + const ROOT_PATH = path.join(__dirname, '../../../'); + + try { + console.log('Checking file access...'); + await fs.access(zipFilePath); + + console.log('Getting file stats...'); + const stats = await fs.stat(zipFilePath); + console.log('File size:', stats.size); + + if (stats.size > MAX_FILE_SIZE) { + console.log('File size exceeds limit'); + return {success: false, error: 'File size exceeds limit'}; + } + + // Copying zip file to /tmp + const tempZipPath = path.join('/tmp', path.basename(zipFilePath)); + await fs.copyFile(zipFilePath, tempZipPath); + + // Launching background update process + const servicesUpdate = (async () => { + try { + console.log('Stopping services...'); + + // await ProjectEventsService.sendEvent('SERVICE_STOP_STARTED', { + // message: 'Stopping services', + // timestamp: new Date().toISOString() + // }); + + await stopServices(); + + // await ProjectEventsService.sendEvent('SERVICE_STOP_COMPLETED', { + // message: 'Services stopped successfully', + // timestamp: new Date().toISOString() + // }); + + console.log('Creating zip instance...'); + const zip = new AdmZip(tempZipPath); + + console.log('Extracting files to:', ROOT_PATH); + zip.extractAllTo(ROOT_PATH, true); + console.log('Files extracted'); + + const removedFilesPath = path.join(ROOT_PATH, 'removed_files.json'); + try { + await fs.access(removedFilesPath); + const removedFilesContent = await fs.readFile(removedFilesPath, 'utf8'); + const filesToRemove = JSON.parse(removedFilesContent); + await removeFiles(filesToRemove, ROOT_PATH); + + await fs.unlink(removedFilesPath); + } catch (error) { + console.log('No removed files to process or error accessing removed_files.json:', error); + } + + // Remove temp zip file + await fs.unlink(tempZipPath); + + // await ProjectEventsService.sendEvent('SERVICE_START_STARTED', { + // message: 'Starting services', + // timestamp: new Date().toISOString() + // }); + + // Start services after a delay + setTimeout(async () => { + try { + await startServices(); + console.log('Services started successfully'); + + await ProjectEventsService.sendEvent('SERVICE_START_COMPLETED', { + message: 'All files have been successfully retrieved and applied.', + timestamp: new Date().toISOString() + }); + } catch (e) { + console.error('Failed to start services:', e); + } + }, 3000); + + } catch (error) { + console.error('Error in service update process:', error); + } + })(); + + servicesUpdate.catch(error => { + console.error('Background update process failed:', error); + }); + + console.log('Returning immediate response'); + + return { + success: true, + message: 'Update process initiated' + }; + + } catch (error) { + console.error('Critical error in updateProjectFilesFromScheme:', error); + return { + success: false, + error: error.message + }; + } + } + + static async getDBSchema() { + try { + return await database.getDBSchema(); + } catch (error) { + console.error('Error reading schema:', error); + throw { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async executeSQL(query) { + try { + return await database.executeSQL(query); + } catch (error) { + console.error('Error executing query:', error); + throw { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async stopServices() { + return await stopServices(); + } + + static async startServices() { + return await startServices(); + } + + static async checkServicesStatus() { + return await checkStatus(); + } + + static async searchFiles(searchStrings) { + const results = {}; + const ROOT_PATH = path.join(__dirname, '../../../'); + const directories = [`${ROOT_PATH}backend/`, `${ROOT_PATH}frontend/`]; + const excludeDirs = ['node_modules', 'build', 'app_shell']; + + if (!Array.isArray(searchStrings)) { + searchStrings = [searchStrings]; + } + + for (const searchString of searchStrings) { + try { + for (const directoryPath of directories) { + const findCommand = `find '${directoryPath}' -type f ${excludeDirs.map(dir => `-not -path "*/${dir}/*"`).join(' ')} -print | xargs grep -nH -C 1 -e '${searchString}'`; + + try { + const { stdout } = await execAsync(findCommand); + + const lines = stdout.trim().split('\n').filter(line => line !== ''); + const searchResults = {}; + // searchResults['__raw_lines__'] = lines; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(':'); + let filePath = ''; + let lineNumberStr = ''; + let content = ''; + let relativeFilePath = ''; + let lineNum = null; + + if (parts.length >= 3 && !parts[0].includes('-')) { + filePath = parts.shift(); + lineNumberStr = parts.shift(); + content = parts.join(':').trim(); + relativeFilePath = filePath.replace(`${ROOT_PATH}`, ''); + lineNum = parseInt(lineNumberStr, 10) + 1; + } else { + content = line.trim(); + } + + const context = []; + if (i > 0 && lines[i - 1].includes(':')) { + const prevLineParts = lines[i - 1].split(':'); + if (prevLineParts.length >= 3 && !prevLineParts[0].includes('-')) { + prevLineParts.shift(); + prevLineParts.shift(); + context.push(prevLineParts.join(':').trim()); + } else { + context.push(lines[i - 1].trim()); + } + } + context.push(content); + if (i < lines.length - 1 && lines[i + 1].includes(':')) { + const nextLineParts = lines[i + 1].split(':'); + if (nextLineParts.length >= 3 && !nextLineParts[0].includes('-')) { + nextLineParts.shift(); + nextLineParts.shift(); + context.push(nextLineParts.join(':').trim()); + } else { + context.push(lines[i + 1].trim()); + } + } + + if (relativeFilePath && !searchResults[relativeFilePath]) { + searchResults[relativeFilePath] = []; + } + if (relativeFilePath) { + searchResults[relativeFilePath].push({ + lineNumber: lineNum, + context: context.join('\n'), + // __filePathAndLine__: filePath + ':' + lineNumberStr + ':' + content, + }); + } + } + + if (!results[searchString]) { + results[searchString] = {}; + } + Object.assign(results[searchString], searchResults); + } catch (err) { + if (!err.message.includes('No such file or directory') && !err.stderr.includes('No such file or directory')) { + console.error(`Error using find/grep for "${searchString}" in ${directoryPath}:`, err); + } + } + } + } catch (error) { + console.error(`Error searching for "${searchString}":`, error); + results[searchString] = { error: error.message }; + } + } + + return results; + } + +} + +async function getDirectoryTree(dirPath) { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + const result = {}; + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory() && ( + entry.name === 'node_modules' || + entry.name === 'app-shell' || + entry.name === '.git' || + entry.name === '.idea' + )) { + continue; + } + + const relativePath = fullPath.replace('/app', ''); + + if (entry.isDirectory()) { + const subTree = await getDirectoryTree(fullPath); + Object.keys(subTree).forEach(key => { + result[key.replace('/app', '')] = subTree[key]; + }); + } else { + const fileContent = await fs.readFile(fullPath, 'utf8'); + const lineCount = fileContent.split('\n').length; + result[relativePath] = lineCount; + } + } + + return result; +} + +async function stopServices() { + try { + console.log('Finding service processes...'); + // await ProjectEventsService.sendEvent('SERVICE_STOP_INITIATED', { + // message: 'Initiating service stop', + // timestamp: new Date().toISOString() + // }); + // Frontend stopping + const { stdout: frontendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ext-server' | awk '{print $1}'"); + if (frontendProcess.trim()) { + console.log('Stopping frontend, pid:', frontendProcess.trim()); + + // await ProjectEventsService.sendEvent('FRONTEND_STOP_STARTED', { + // message: `Stopping frontend, pid: ${frontendProcess.trim()}`, + // timestamp: new Date().toISOString() + // }); + + // await execAsync(`kill -15 ${frontendProcess.trim()}`); + + // await ProjectEventsService.sendEvent('FRONTEND_STOP_COMPLETED', { + // message: 'Frontend stopped successfully', + // timestamp: new Date().toISOString() + // }); + } + + // Backend stopping + const { stdout: backendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ode ./src/index.js' | grep -v app-shell | awk '{print $1}'"); + if (backendProcess.trim()) { + console.log('Stopping backend, pid:', backendProcess.trim()); + + // await ProjectEventsService.sendEvent('BACKEND_STOP_STARTED', { + // message: `Stopping backend, pid: ${backendProcess.trim()}`, + // timestamp: new Date().toISOString() + // }); + + // await execAsync(`kill -15 ${backendProcess.trim()}`); + + // await ProjectEventsService.sendEvent('BACKEND_STOP_COMPLETED', { + // message: 'Backend stopped successfully', + // timestamp: new Date().toISOString() + // }); + } + + await new Promise(resolve => setTimeout(resolve, 4000)); + + + // await ProjectEventsService.sendEvent('SERVICE_STOP_COMPLETED', { + // message: 'All services stopped successfully', + // timestamp: new Date().toISOString() + // }); + + return { success: true }; + } catch (error) { + console.error('Error stopping services:', error); + + await ProjectEventsService.sendEvent('SERVICE_STOP_FAILED', { + message: 'Error stopping services', + error: error.message, + timestamp: new Date().toISOString() + }); + + return { success: false, error: error.message }; + } +} + +async function startServices() { + try { + console.log('Starting services...'); + // await ProjectEventsService.sendEvent('SERVICE_START_INITIATED', { + // message: 'Initiating service start', + // timestamp: new Date().toISOString() + // }); + + // await ProjectEventsService.sendEvent('FRONTEND_START_STARTED', { + // message: 'Starting frontend service', + // timestamp: new Date().toISOString() + // }); + // await execAsync('yarn --cwd /app/frontend dev &'); + // await ProjectEventsService.sendEvent('FRONTEND_START_COMPLETED', { + // message: 'Frontend service started successfully', + // timestamp: new Date().toISOString() + // }); + + // await ProjectEventsService.sendEvent('BACKEND_START_STARTED', { + // message: 'Starting backend service', + // timestamp: new Date().toISOString() + // }); + // await execAsync('yarn --cwd /app/backend start &'); + // await ProjectEventsService.sendEvent('BACKEND_START_COMPLETED', { + // message: 'Backend service started successfully', + // timestamp: new Date().toISOString() + // }); + + // await ProjectEventsService.sendEvent('SERVICE_START_COMPLETED', { + // message: 'All services started successfully', + // timestamp: new Date().toISOString() + // }); + + return { success: true }; + } catch (error) { + console.error('Error starting services:', error); + await ProjectEventsService.sendEvent('SERVICE_START_FAILED', { + message: 'Error starting services', + error: error.message, + timestamp: new Date().toISOString() + }); + return { success: false, error: error.message }; + } +} + +async function checkStatus() { + try { + const { stdout } = await execAsync('ps aux'); + return { + success: true, + frontendRunning: stdout.includes('next-server'), + backendRunning: stdout.includes('nodemon') && stdout.includes('/app/backend'), + nginxRunning: stdout.includes('nginx: master process') + }; + } catch (error) { + return { + success: false, + error: error.message + }; + } +} + +async function validateJSXSyntax(code) { + // Define validation rules for JSX + const rules = [ + { + // JSX attribute with expression + pattern: /^[a-zA-Z][a-zA-Z0-9]*={.*}$/, + message: 'Invalid JSX attribute syntax' + }, + { + // Invalid sequences + pattern: /,{2,}/, + message: 'Invalid character sequence detected', + shouldNotMatch: true + }, + { + // Ternary expressions + pattern: /^[a-zA-Z][a-zA-Z0-9]*={[\w\s]+\?[^}]+:[^}]+}$/, + message: 'Invalid ternary expression in JSX' + } + ]; + + // Validate each line + const lines = code.split('\n'); + for (const line of lines) { + const trimmedLine = line.trim(); + + // Skip empty lines + if (!trimmedLine) continue; + + // Check each rule + for (const rule of rules) { + if (rule.shouldNotMatch) { + // For patterns that should not be present + if (rule.pattern.test(trimmedLine)) { + return { + valid: false, + errors: [{ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: rule.message + }] + }; + } + } else { + // For patterns that should match + if (trimmedLine.includes('=') && !rule.pattern.test(trimmedLine)) { + return { + valid: false, + errors: [{ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: rule.message + }] + }; + } + } + } + + // Additional JSX-specific checks + if ((trimmedLine.match(/{/g) || []).length !== (trimmedLine.match(/}/g) || []).length) { + return { + valid: false, + errors: [{ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: 'Unmatched curly braces in JSX' + }] + }; + } + } + + // If all checks pass + return { + valid: true, + errors: [] + }; +} + +async function removeFiles(files, rootPath) { + try { + for (const file of files) { + const fullPath = path.join(rootPath, file); + try { + await fs.unlink(fullPath); + console.log(`File removed: ${fullPath}`); + } catch (error) { + console.error(`Error when trying to delete a file ${fullPath}:`, error); + } + } + } catch (error) { + console.error('Error removing files:', error); + throw error; + } +} \ No newline at end of file diff --git a/app-shell/src/services/notifications/errors/forbidden.js b/app-shell/src/services/notifications/errors/forbidden.js new file mode 100644 index 0000000..192fa10 --- /dev/null +++ b/app-shell/src/services/notifications/errors/forbidden.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ForbiddenError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.forbidden.message'); + + super(message); + this.code = 403; + } +}; diff --git a/app-shell/src/services/notifications/errors/validation.js b/app-shell/src/services/notifications/errors/validation.js new file mode 100644 index 0000000..464550c --- /dev/null +++ b/app-shell/src/services/notifications/errors/validation.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ValidationError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.validation.message'); + + super(message); + this.code = 400; + } +}; diff --git a/app-shell/src/services/notifications/helpers.js b/app-shell/src/services/notifications/helpers.js new file mode 100644 index 0000000..1c3a60f --- /dev/null +++ b/app-shell/src/services/notifications/helpers.js @@ -0,0 +1,30 @@ +const _get = require('lodash/get'); +const errors = require('./list'); + +function format(message, args) { + if (!message) { + return null; + } + + return message.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? args[number] : match; + }); +} + +const isNotification = (key) => { + const message = _get(errors, key); + return !!message; +}; + +const getNotification = (key, ...args) => { + const message = _get(errors, key); + + if (!message) { + return key; + } + + return format(message, args); +}; + +exports.getNotification = getNotification; +exports.isNotification = isNotification; diff --git a/app-shell/src/services/notifications/list.js b/app-shell/src/services/notifications/list.js new file mode 100644 index 0000000..a0a1613 --- /dev/null +++ b/app-shell/src/services/notifications/list.js @@ -0,0 +1,100 @@ +const errors = { + app: { + title: 'test', + }, + + auth: { + userDisabled: 'Your account is disabled', + forbidden: 'Forbidden', + unauthorized: 'Unauthorized', + userNotFound: `Sorry, we don't recognize your credentials`, + wrongPassword: `Sorry, we don't recognize your credentials`, + weakPassword: 'This password is too weak', + emailAlreadyInUse: 'Email is already in use', + invalidEmail: 'Please provide a valid email', + passwordReset: { + invalidToken: 'Password reset link is invalid or has expired', + error: `Email not recognized`, + }, + passwordUpdate: { + samePassword: `You can't use the same password. Please create new password`, + }, + userNotVerified: `Sorry, your email has not been verified yet`, + emailAddressVerificationEmail: { + invalidToken: 'Email verification link is invalid or has expired', + error: `Email not recognized`, + }, + }, + + iam: { + errors: { + userAlreadyExists: 'User with this email already exists', + userNotFound: 'User not found', + disablingHimself: `You can't disable yourself`, + revokingOwnPermission: `You can't revoke your own owner permission`, + deletingHimself: `You can't delete yourself`, + emailRequired: 'Email is required', + }, + }, + + importer: { + errors: { + invalidFileEmpty: 'The file is empty', + invalidFileExcel: 'Only excel (.xlsx) files are allowed', + invalidFileUpload: + 'Invalid file. Make sure you are using the last version of the template.', + importHashRequired: 'Import hash is required', + importHashExistent: 'Data has already been imported', + userEmailMissing: 'Some items in the CSV do not have an email', + }, + }, + + errors: { + forbidden: { + message: 'Forbidden', + }, + validation: { + message: 'An error occurred', + }, + searchQueryRequired: { + message: 'Search query is required', + }, + }, + + emails: { + invitation: { + subject: `You've been invited to {0}`, + body: ` +

Hello,

+

You've been invited to {0} set password for your {1} account.

+

{2}

+

Thanks,

+

Your {0} team

+ `, + }, + emailAddressVerification: { + subject: `Verify your email for {0}`, + body: ` +

Hello,

+

Follow this link to verify your email address.

+

{0}

+

If you didn't ask to verify this address, you can ignore this email.

+

Thanks,

+

Your {1} team

+ `, + }, + passwordReset: { + subject: `Reset your password for {0}`, + body: ` +

Hello,

+

Follow this link to reset your {0} password for your {1} account.

+

{2}

+

If you didn't ask to reset your password, you can ignore this email.

+

Thanks,

+

Your {0} team

+ `, + }, + }, +}; + +module.exports = errors; diff --git a/app-shell/src/services/project-events.js b/app-shell/src/services/project-events.js new file mode 100644 index 0000000..dabc32d --- /dev/null +++ b/app-shell/src/services/project-events.js @@ -0,0 +1,67 @@ +const axios = require('axios'); +const config = require('../config.js'); + +class ProjectEventsService { + /** + * Sends a project event to the Rails backend + * + * @param {string} eventType - Type of the event + * @param {object} payload - Event payload data + * @param {object} options - Additional options + * @param {string} [options.conversationId] - Optional conversation ID + * @param {boolean} [options.isError=false] - Whether this is an error event + * @returns {Promise} - Response from the webhook + */ + static async sendEvent(eventType, payload = {}, options = {}) { + try { + console.log(`[DEBUG] Sending project event: ${eventType}`); + + const webhookUrl = `https://flatlogic.com/projects/events_webhook`; + + // Prepare the event data + const eventData = { + project_uuid: config.project_uuid, + event_type: eventType, + payload: { + ...payload, + message: `[APP] ${payload.message}`, + is_error: options.isError || false, + system_message: true, + is_command_info: true + } + }; + + // Add conversation ID if provided + if (options.conversationId) { + eventData.conversation_id = options.conversationId; + } + + const headers = { + 'Content-Type': 'application/json', + 'x-project-uuid': config.project_uuid + }; + + console.log(`[DEBUG] Event data: ${JSON.stringify(eventData)}`); + + const response = await axios.post(webhookUrl, eventData, { headers }); + + console.log(`[DEBUG] Event sent successfully, status: ${response.status}`); + return response.data; + } catch (error) { + console.error(`[ERROR] Failed to send project event: ${error.message}`); + if (error.response) { + console.error(`[ERROR] Response status: ${error.response.status}`); + console.error(`[ERROR] Response data: ${JSON.stringify(error.response.data)}`); + } + + // Don't throw the error, just return a failed status + // This prevents errors in the event service from breaking app functionality + return { + success: false, + error: error.message + }; + } + } +} + +module.exports = ProjectEventsService; \ No newline at end of file diff --git a/app-shell/src/services/vcs.js b/app-shell/src/services/vcs.js new file mode 100644 index 0000000..6235c57 --- /dev/null +++ b/app-shell/src/services/vcs.js @@ -0,0 +1,1205 @@ +const util = require('util'); +const exec = util.promisify(require('child_process').exec); +const path = require('path'); +const { promises: fs } = require("fs"); +const axios = require('axios'); +const config = require('../config.js'); + +const ROOT_PATH = '/app'; +const MAX_BUFFER = 1024 * 1024 * 50; +const GITEA_DOMAIN = config.gitea_domain; +const USERNAME = config.gitea_username; +const API_TOKEN = config.gitea_api_token; +const GITHUB_REPO_URL = config.github_repo_url; +const GITHUB_TOKEN = config.github_token; + +const devSchemaFilePath = path.join(ROOT_PATH, '/app-shell/src/_schema.json'); + +class VCS { + static isInitRepoRunning = false; + // Main method – controller of the repository initialization process + static async initRepo(projectId = 'test') { + if (VCS.isInitRepoRunning) { + console.warn('[WARNING] initRepo is already running. Skipping.'); + return; + } + VCS.isInitRepoRunning = true; + try { + console.log(`[DEBUG] Starting repository initialization for project "${projectId}"...`); + + await this._waitForGitLockRelease(path.join(ROOT_PATH, '.git')); + // await this._removeGitLockIfExists(path.join(ROOT_PATH, '.git')); + console.log('[DEBUG] Git lock released, proceeding with initialization...'); + + if (GITHUB_REPO_URL) { + console.log(`[DEBUG] GitHub repository URL provided: ${GITHUB_REPO_URL}`); + console.log(`[DEBUG] Setting up local GitHub repository...`); + await this.setupLocalGitHubRepo(); + console.log(`[DEBUG] GitHub repository setup completed.`); + } else { + console.log(`[DEBUG] No GitHub repository URL provided. Skipping GitHub setup.`); + } + + console.log(`[DEBUG] Setting up Gitea remote repository for project "${projectId}"...`); + const giteaRemoteUrl = await this.setupGiteaRemote(projectId); + console.log(`[DEBUG] Gitea remote URL: ${giteaRemoteUrl.replace(/\/\/.*?@/, '//***@')}`); + + if (!GITHUB_REPO_URL) { + console.log(`[DEBUG] Setting up local repository with Gitea remote...`); + await this.setupLocalRepo(giteaRemoteUrl); + console.log(`[DEBUG] Local repository setup with Gitea remote completed.`); + } else { + console.log(`[DEBUG] Adding Gitea as additional remote to existing GitHub repository...`); + await this._addGiteaRemote(giteaRemoteUrl); + console.log(`[DEBUG] Gitea remote added to GitHub repository.`); + } + + console.log(`[DEBUG] Repository initialization for project "${projectId}" completed successfully.`); + console.log(`[DEBUG] Repository configuration: GitHub: ${GITHUB_REPO_URL ? 'Yes' : 'No'}, Gitea: Yes`); + + return { message: `Repository ${projectId} is ready.` }; + } catch (error) { + console.error(`[ERROR] Repository initialization for project "${projectId}" failed: ${error?.message}`); + + throw new Error(`Error during repo initialization: ${error.message}`); + } finally { + VCS.isInitRepoRunning = false; + console.log(`[DEBUG] Repository initialization process for "${projectId}" finished.`); + } + } + + // Checks for the existence of the remote repo and creates it if it doesn't exist + static async setupGiteaRemote(projectId) { + console.log(`[DEBUG] Checking Gitea remote repository "${projectId}"...`); + let repoData = await this.checkRepoExists(projectId); + if (!repoData) { + console.log(`[DEBUG] Gitea remote repository "${projectId}" does not exist. Creating...`); + repoData = await this.createRemoteRepo(projectId); + console.log(`[DEBUG] Gitea remote repository created: ${JSON.stringify(repoData)}`); + } else { + console.log(`[DEBUG] Gitea remote repository "${projectId}" already exists.`); + } + // Return the URL with token authentication + return `https://${USERNAME}:${API_TOKEN}@${GITEA_DOMAIN}/${USERNAME}/${projectId}.git`; + } + + // Sets up the local repository: either fetches/reset if .git exists, + // initializes git in a non-empty directory, or clones the repository if empty. + static async setupLocalRepo(remoteUrl) { + const gitDir = path.join(ROOT_PATH, '.git'); + const localRepoExists = await this.exists(gitDir); + if (localRepoExists) { + await this.fetchAndResetRepo(); + } else { + const files = await fs.readdir(ROOT_PATH); + if (files.length > 0) { + await this.initializeGitRepo(remoteUrl); + } else { + console.log('[DEBUG] Local directory is empty. Cloning remote repository...'); + await exec(`git clone ${remoteUrl} .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + } + } + + static async setupLocalGitHubRepo() { + try { + if (!GITHUB_REPO_URL) { + console.log('[DEBUG] GITHUB_REPO_URL is not set. Skipping GitHub repo setup.'); + return; + } + + const gitDir = path.join(ROOT_PATH, '.git'); + const repoExists = await this.exists(gitDir); + + if (repoExists) { + console.log('[DEBUG] Git repository already initialized. Fetching and resetting...'); + + await this._addGithubRemote(); + + console.log('[DEBUG] Fetching GitHub remote...'); + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + try { + console.log('[DEBUG] Checking for remote branch "github/ai-dev"...'); + await exec(`git rev-parse --verify github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Remote branch "github/ai-dev" exists. Resetting local repository to github/ai-dev...'); + await exec(`git reset --hard github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (e) { + console.log('[DEBUG] Remote branch "github/ai-dev" does NOT exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + return; + } + + console.log('[DEBUG] Initializing git in existing directory...'); + // const gitignorePath = path.join(ROOT_PATH, '.gitignore'); + // const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`; + // await fs.writeFile(gitignorePath, ignoreContent, 'utf8'); + + await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Configuring git user...'); + await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + await this._addGithubRemote(); + + console.log('[DEBUG] Fetching GitHub remote...'); + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + try { + console.log('[DEBUG] Checking for remote branch "github/ai-dev"...'); + await exec(`git rev-parse --verify github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Remote branch "github/ai-dev" exists. Resetting local repository to github/ai-dev...'); + await exec(`git reset --hard github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (e) { + console.log('[DEBUG] Remote branch "github/ai-dev" does NOT exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + } catch (error) { + console.error(`[ERROR] Failed to setup local GitHub repo: ${error?.message}`); + throw error; + } + } + + // Check if a file/directory exists + static async exists(pathToCheck) { + try { + await fs.access(pathToCheck); + return true; + } catch { + return false; + } + } + + // If the local repository exists, fetches remote data and resets the repository state + static async fetchAndResetRepo() { + console.log('[DEBUG] Local repository exists. Fetching remotes...'); + + if (GITHUB_REPO_URL) { + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchReset = await this.tryResetToBranch('ai-dev', 'github'); + + if (branchReset) { + return; + } + } + + await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchReset = await this.tryResetToBranch('ai-dev', 'gitea'); + + if (!branchReset) { + const masterReset = await this.tryResetToBranch('master', 'gitea'); + if (masterReset) { + console.log('[DEBUG] Creating and switching to branch "ai-dev"...'); + await exec(`git branch ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Pushing ai-dev branch to remotes...'); + await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (GITHUB_REPO_URL) { + await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + } else { + console.log('[DEBUG] Neither "gitea/ai-dev" nor "gitea/master" exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + } + } + + // Tries to check out and reset to the specified branch + static async tryResetToBranch(branchName, remote) { + try { + console.log(`[DEBUG] Checking for remote branch "${remote}/${branchName}"...`); + await exec(`git rev-parse --verify ${remote}/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remote branch "${remote}/${branchName}" found. Resetting local repository to "${remote}/${branchName}"...`); + await exec(`git reset --hard ${remote}/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git checkout ${branchName === 'ai-dev' ? 'ai-dev' : branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + return true; + } catch (e) { + console.log(`[DEBUG] Remote branch "${remote}/${branchName}" does NOT exist.`); + + return false; + } + } + + // If remote branch doesn't exist, make the initial commit and set up branches + static async commitInitialChanges() { + console.log('[DEBUG] Adding all files for initial commit...'); + await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const { stdout: status } = await exec(`git status --porcelain`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (status.trim()) { + await exec(`git commit -m "Initial version"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (GITHUB_REPO_URL) { + await exec(`git push -u github master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + await exec(`git push -u gitea master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log('[DEBUG] Creating and switching to branch "ai-dev"...'); + await exec(`git branch ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Making ai-dev branch identical to master...'); + + if (GITHUB_REPO_URL) { + await exec(`git reset --hard github/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + await exec(`git reset --hard gitea/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + console.log('[DEBUG] Pushing ai-dev branch to remotes...'); + if (GITHUB_REPO_URL) { + await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + console.log('[DEBUG] No local changes to commit.'); + } + } + + // If the local directory is not empty but .git doesn't exist, initialize git, + // add .gitignore, configure the user, and add the remote origin. + static async initializeGitRepo(giteaRemoteUrl) { + console.log('[DEBUG] Local directory is not empty. Initializing git...'); + const gitignorePath = path.join(ROOT_PATH, '.gitignore'); + const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`; + await fs.writeFile(gitignorePath, ignoreContent, 'utf8'); + + await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Configuring git user...'); + await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log(`[DEBUG] Adding Gitea remote ${giteaRemoteUrl}...`); + await exec(`git remote add gitea ${giteaRemoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (GITHUB_REPO_URL) { + await this._addGithubRemote(); + } + + console.log('[DEBUG] Fetching Gitea remote...'); + await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + try { + console.log('[DEBUG] Checking for remote branch "gitea/ai-dev"...'); + await exec(`git rev-parse --verify gitea/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Remote branch "gitea/ai-dev" exists. Resetting local repository to gitea/ai-dev...'); + await exec(`git reset --hard gitea/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (e) { + console.log('[DEBUG] Remote branch "gitea/ai-dev" does NOT exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + } + + // Method to check if the repository exists on remote server + static async checkRepoExists(repoName) { + const url = `https://${GITEA_DOMAIN}/api/v1/repos/${USERNAME}/${repoName}`; + try { + const response = await axios.get(url, { + headers: { Authorization: `token ${API_TOKEN}` } + }); + return response.data; + } catch (err) { + if (err.response && err.response.status === 404) { + return null; + } + throw new Error('Error checking repository existence: ' + err?.message); + } + } + + // Method to create a remote repository via API + static async createRemoteRepo(repoName) { + const createUrl = `https://${GITEA_DOMAIN}/api/v1/user/repos`; + console.log("[DEBUG] createUrl", createUrl); + + try { + const response = await axios.post(createUrl, { + name: repoName, + description: `Repository for project ${repoName}`, + private: false + }, { + headers: { Authorization: `token ${API_TOKEN}` } + }); + + return response.data; + } catch (err) { + console.log('Error creating repository via API: ' + err?.message) + // throw new Error('Error creating repository via API: ' + err.message); + } + } + + static async commitChanges(message = "", files = '.', dev_schema) { + try { + console.log(`[DEBUG] Starting commit process...`); + await this._ensureDevBranch(); + + console.log(`[DEBUG] Ensuring .gitignore is properly configured...`); + await this._ensureGitignore(); + + // Save dev_schema + await this._saveDevSchema(message, dev_schema); + + console.log(`[DEBUG] Adding files to git index: ${files}`); + if (files === '.') { + await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + await exec(`git add ${files}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + const { stdout: status } = await exec('git status --porcelain', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Git status before commit: ${status}`); + + if (!status.trim()) { + console.log(`[DEBUG] No changes to commit`); + return { message: "No changes to commit" }; + } + + const now = new Date(); + const commitMessage = message || `Auto commit: ${now.toISOString()}`; + console.log(`[DEBUG] Committing changes with message: "${commitMessage}"`); + + const { stdout: commitOutput, stderr: commitError } = await exec(`git commit -m "${commitMessage}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Commit output: ${commitOutput}`); + if (commitError) { + console.log(`[DEBUG] Commit stderr: ${commitError}`); + } + + const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchName = currentBranch.trim(); + console.log(`[DEBUG] Current branch: ${branchName}`); + + console.log(`[DEBUG] Pushing changes to Gitea...`); + try { + const { stdout: giteaPushOutput, stderr: giteaPushError } = await exec(`git push gitea ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea push output: ${giteaPushOutput}`); + if (giteaPushError) { + console.log(`[DEBUG] Gitea push stderr: ${giteaPushError}`); + } + } catch (giteaError) { + console.error(`[ERROR] Failed to push to Gitea: ${giteaError?.message}`); + + if (giteaError.stderr && giteaError.stderr.includes('rejected')) { + console.log(`[DEBUG] Push rejected, trying with --force...`); + try { + const { stdout, stderr } = await exec(`git push gitea ${branchName} --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to Gitea output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to Gitea stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to Gitea failed: ${forceError?.message}`); + } + } + } + + if (GITHUB_REPO_URL) { + console.log(`[DEBUG] Pushing changes to GitHub...`); + try { + const { stdout: githubPushOutput, stderr: githubPushError } = await exec(`git push github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub push output: ${githubPushOutput}`); + if (githubPushError) { + console.log(`[DEBUG] GitHub push stderr: ${githubPushError}`); + } + } catch (githubError) { + console.error(`[ERROR] Failed to push to GitHub: ${githubError?.message}`); + + if (githubError.stderr && githubError.stderr.includes('rejected')) { + console.log(`[DEBUG] Push rejected, trying with --force...`); + try { + const { stdout, stderr } = await exec(`git push github ${branchName} --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to GitHub output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to GitHub stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to GitHub failed: ${forceError?.message}`); + } + } + } + } + + console.log(`[DEBUG] Commit process completed`); + return { message: "Changes committed" }; + } catch (error) { + console.error(`[ERROR] Error during commit process: ${error?.message}`); + } + } + + static async getLog() { + try { + const remote = GITHUB_REPO_URL ? 'github' : 'gitea'; + + const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remotes: ${remotes}`); + + const { stdout: branches } = await exec(`git branch -a`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Branches: ${branches}`); + + const { stdout } = await exec(`git log ${remote}/ai-dev --oneline`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const lines = stdout.split(/\r?\n/).filter(line => line.trim() !== ''); + const result = {}; + lines.forEach((line) => { + const firstSpaceIndex = line.indexOf(' '); + if (firstSpaceIndex > 0) { + const hash = line.substring(0, firstSpaceIndex); + const message = line.substring(firstSpaceIndex + 1).trim(); + result[hash] = message; + } + }); + return result; + } catch (error) { + console.error(`[ERROR] Error during get log: ${error?.message}`); + throw error; + } + } + + static async checkout(ref) { + try { + await exec(`git checkout ${ref}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + return { message: `Checked out to ${ref}` }; + } catch (error) { + throw new Error(`Error during checkout: ${error?.message}`); + } + } + + static async revert(commitHash) { + try { + const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchName = currentBranch.trim(); + + await exec(`git reset --hard`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + await exec( + `git revert --no-edit ${commitHash}..HEAD --no-commit`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + + await exec( + `git commit -m "Revert to version ${commitHash}"`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + + await exec(`git push gitea ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (GITHUB_REPO_URL) { + await exec(`git push github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + const commitMessage = await this._getCommitMessageByHash(commitHash); + const devSchema = await this._getDevSchemaByCommitMessage(commitMessage); + + return { message: `Reverted to commit ${commitHash}`, devSchema }; + } catch (error) { + console.error("Error during revert:", error?.message); + if (error.stdout) { + console.error("Revert stdout:", error.stdout); + } + if (error.stderr) { + console.error("Revert stderr:", error.stderr); + } + throw new Error(`Error during revert: ${error?.message}`); + } + } + + static async mergeDevIntoMaster() { + try { + // First, make sure we have the latest changes from both branches + console.log('[DEBUG] Fetching latest changes from remote repositories...'); + await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (GITHUB_REPO_URL) { + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + // Switch to branch 'master' + console.log('[DEBUG] Switching to branch "master"...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Pull latest changes from master + console.log('[DEBUG] Pulling latest changes from master branch...'); + try { + await exec(`git pull gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Successfully pulled from Gitea master'); + } catch (pullError) { + console.warn(`[WARN] Failed to pull from Gitea master: ${pullError?.message}`); + // Try to continue anyway + } + + // Switch to ai-dev and make sure it's up to date + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Pull latest changes from ai-dev + console.log('[DEBUG] Pulling latest changes from ai-dev branch...'); + try { + await exec(`git pull gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Successfully pulled from Gitea ai-dev'); + } catch (pullError) { + console.warn(`[WARN] Failed to pull from Gitea ai-dev: ${pullError?.message}`); + // Try to continue anyway + } + + // Switch back to master for the merge + console.log('[DEBUG] Switching back to branch "master"...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Merge branch 'ai-dev' into 'master' with a forced merge. + // Parameter -X theirs is used to resolve conflicts by keeping the changes from the branch being merged in case of conflicts. + console.log('[DEBUG] Merging branch "ai-dev" into "master" (force merge with -X theirs)...'); + try { + const { stdout: mergeOutput, stderr: mergeError } = await exec( + `git merge ai-dev --no-ff -X theirs -m "Forced merge: merge ai-dev into master"`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + console.log(`[DEBUG] Merge output: ${mergeOutput}`); + if (mergeError) { + console.log(`[DEBUG] Merge stderr: ${mergeError}`); + } + } catch (mergeError) { + console.error(`[ERROR] Merge failed: ${mergeError?.message}`); + if (mergeError.stdout) { + console.error(`[ERROR] Merge stdout: ${mergeError.stdout}`); + } + if (mergeError.stderr) { + console.error(`[ERROR] Merge stderr: ${mergeError.stderr}`); + } + + // Abort the merge if it failed + console.log('[DEBUG] Aborting failed merge...'); + await exec(`git merge --abort`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + throw new Error(`Failed to merge ai-dev into master: ${mergeError?.message}`); + } + + // Push the merged 'master' branch to both remotes + console.log('[DEBUG] Pushing merged master branch to Gitea remote...'); + try { + const { stdout: giteaPushOutput, stderr: giteaPushError } = await exec(`git push gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea push output: ${giteaPushOutput}`); + if (giteaPushError) { + console.log(`[DEBUG] Gitea push stderr: ${giteaPushError}`); + } + } catch (pushError) { + console.error(`[ERROR] Failed to push to Gitea: ${pushError?.message}`); + + // If push is rejected, try with --force + if (pushError.stderr && pushError.stderr.includes('rejected')) { + console.log('[DEBUG] Push rejected, trying with --force...'); + try { + const { stdout, stderr } = await exec(`git push gitea master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to Gitea output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to Gitea stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to Gitea also failed: ${forceError?.message}`); + throw forceError; + } + } else { + throw pushError; + } + } + + if (GITHUB_REPO_URL) { + console.log('[DEBUG] Pushing merged master branch to GitHub remote...'); + try { + const { stdout: githubPushOutput, stderr: githubPushError } = await exec(`git push github master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub push output: ${githubPushOutput}`); + if (githubPushError) { + console.log(`[DEBUG] GitHub push stderr: ${githubPushError}`); + } + } catch (pushError) { + console.error(`[ERROR] Failed to push to GitHub: ${pushError?.message}`); + + // If push is rejected, try with --force + if (pushError.stderr && pushError.stderr.includes('rejected')) { + console.log('[DEBUG] Push rejected, trying with --force...'); + try { + const { stdout, stderr } = await exec(`git push github master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to GitHub output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to GitHub stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to GitHub also failed: ${forceError?.message}`); + throw forceError; + } + } else { + throw pushError; + } + } + } + + return { message: "Branch ai-dev merged into master and pushed to all remotes" }; + } catch (error) { + console.error(`[ERROR] Error during mergeDevIntoMaster: ${error?.message}`); + throw new Error(`Error during merge of ai-dev into master: ${error?.message}`); + } + } + + static async _mergeDevIntoMasterGitHub() { + try { + // Switch to branch 'master' + console.log('Switching to branch "master" (GitHub)...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Merge branch 'ai-dev' into 'master' with a forced merge. + console.log('Merging branch "ai-dev" into "master" (GitHub, force merge with -X theirs)...'); + await exec( + `git merge ai-dev --no-ff -X theirs -m "Forced merge: merge ai-dev into master"`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + + // Push the merged 'master' branch to remote (GitHub) + console.log('Pushing merged master branch to remote (GitHub)...'); + const { stdout, stderr } = await exec(`git push -f github master`, { + cwd: ROOT_PATH, + maxBuffer: MAX_BUFFER + }); + if (stdout) { + console.log("Git push GitHub stdout:", stdout); + } + if (stderr) { + console.error("Git push GitHub stderr:", stderr); + } + return { message: "Branch ai-dev merged into master and pushed to GitHub remote" }; + } catch (error) { + console.error("Error during mergeDevIntoMasterGitHub:", error?.message); + if (error.stdout) { + console.error("Merge GitHub stdout:", error.stdout); + } + if (error.stderr) { + console.error("Merge GitHub stderr:", error.stderr); + } + throw error; + } + } + + static async resetDevBranch() { + try { + console.log(`[DEBUG] Starting reset of ai-dev branch to match master...`); + + // First, fetch all remote branches to ensure we have the latest information + console.log(`[DEBUG] Fetching latest changes from remotes...`); + await exec(`git fetch --all`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Check current branch state + const { stdout: initialBranches } = await exec(`git branch -a`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Initial branches: ${initialBranches}`); + + // Check if master branch exists + const { stdout: masterExists } = await exec(`git branch --list master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (!masterExists.trim()) { + console.log(`[DEBUG] Master branch does not exist. Creating it...`); + await exec(`git checkout -b master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + // Switch to master branch + console.log(`[DEBUG] Switching to branch "master"...`); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Pull latest changes from master + console.log(`[DEBUG] Pulling latest changes from master...`); + try { + await exec(`git pull gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (error) { + console.log(`[DEBUG] Error pulling from master: ${error?.message}`); + } + + // Verify we are on master branch + const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Current branch after checkout: ${currentBranch.trim()}`); + + // Get master branch commit hash + const { stdout: masterCommit } = await exec(`git rev-parse master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Master branch commit hash: ${masterCommit.trim()}`); + + // Delete local ai-dev branch if it exists + try { + await exec(`git branch -D ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Local branch ai-dev deleted successfully`); + } catch (error) { + console.log(`[DEBUG] Local branch ai-dev does not exist or could not be deleted: ${error?.message}`); + } + + // Create new ai-dev branch from master using the exact commit hash + await exec(`git branch ai-dev ${masterCommit.trim()}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Created new ai-dev branch from master commit ${masterCommit.trim()}`); + + // Switch to the new ai-dev branch + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Switched to new ai-dev branch`); + + // Verify we are on ai-dev branch + const { stdout: newCurrentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Current branch after creating ai-dev: ${newCurrentBranch.trim()}`); + + // Verify that ai-dev points to the same commit as master + const { stdout: aiDevCommit } = await exec(`git rev-parse ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] ai-dev branch commit hash: ${aiDevCommit.trim()}`); + + if (aiDevCommit.trim() !== masterCommit.trim()) { + console.error(`[ERROR] ai-dev branch does not point to the same commit as master!`); + console.error(`[ERROR] master: ${masterCommit.trim()}, ai-dev: ${aiDevCommit.trim()}`); + throw new Error(`Failed to create ai-dev branch from master`); + } + + console.log(`[DEBUG] Verified: ai-dev branch points to the same commit as master`); + + // Delete remote ai-dev branches if they exist + console.log(`[DEBUG] Deleting remote ai-dev branches if they exist...`); + + // For Gitea + try { + // First check if the remote branch exists + const { stdout: giteaBranches } = await exec(`git ls-remote --heads gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (giteaBranches.trim()) { + console.log(`[DEBUG] Remote branch ai-dev exists on Gitea, deleting it...`); + await exec(`git push gitea --delete ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remote branch ai-dev on Gitea deleted successfully`); + + // Verify deletion + const { stdout: verifyGiteaDeletion } = await exec(`git ls-remote --heads gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (verifyGiteaDeletion.trim()) { + console.log(`[WARN] Remote branch ai-dev on Gitea still exists after deletion attempt`); + } else { + console.log(`[DEBUG] Verified: Remote branch ai-dev on Gitea is deleted`); + } + } else { + console.log(`[DEBUG] Remote branch ai-dev does not exist on Gitea`); + } + } catch (error) { + console.log(`[DEBUG] Error checking/deleting remote branch on Gitea: ${error?.message}`); + } + + // For GitHub + if (GITHUB_REPO_URL) { + try { + // First check if the remote branch exists + const { stdout: githubBranches } = await exec(`git ls-remote --heads github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (githubBranches.trim()) { + console.log(`[DEBUG] Remote branch ai-dev exists on GitHub, deleting it...`); + await exec(`git push github --delete ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remote branch ai-dev on GitHub deleted successfully`); + + // Verify deletion + const { stdout: verifyGithubDeletion } = await exec(`git ls-remote --heads github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (verifyGithubDeletion.trim()) { + console.log(`[WARN] Remote branch ai-dev on GitHub still exists after deletion attempt`); + } else { + console.log(`[DEBUG] Verified: Remote branch ai-dev on GitHub is deleted`); + } + } else { + console.log(`[DEBUG] Remote branch ai-dev does not exist on GitHub`); + } + } catch (error) { + console.log(`[DEBUG] Error checking/deleting remote branch on GitHub: ${error?.message}`); + } + } + + // Wait a moment to ensure deletion is processed + console.log(`[DEBUG] Waiting for remote branch deletion to be processed...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Push new ai-dev branch to remote repositories with force + console.log(`[DEBUG] Pushing new ai-dev branch to Gitea (force push)...`); + try { + const { stdout, stderr } = await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea force push output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Gitea force push stderr: ${stderr}`); + } + + // Verify the push + const { stdout: verifyGiteaPush } = await exec(`git ls-remote --heads gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea ai-dev branch after push: ${verifyGiteaPush}`); + + // Extract the hash from the output + const giteaAiDevHash = verifyGiteaPush.split(/\s+/)[0]; + + if (giteaAiDevHash === masterCommit.trim()) { + console.log(`[DEBUG] Verified: Gitea ai-dev branch matches master branch`); + } else { + console.log(`[WARN] Gitea ai-dev branch does not match master branch!`); + console.log(`[WARN] master: ${masterCommit.trim()}, Gitea ai-dev: ${giteaAiDevHash}`); + } + } catch (error) { + console.error(`[ERROR] Force push to Gitea failed: ${error?.message}`); + throw error; + } + + if (GITHUB_REPO_URL) { + console.log(`[DEBUG] Pushing new ai-dev branch to GitHub (force push)...`); + try { + const { stdout, stderr } = await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub force push output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] GitHub force push stderr: ${stderr}`); + } + + // Verify the push + const { stdout: verifyGithubPush } = await exec(`git ls-remote --heads github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub ai-dev branch after push: ${verifyGithubPush}`); + + // Extract the hash from the output + const githubAiDevHash = verifyGithubPush.split(/\s+/)[0]; + + if (githubAiDevHash === masterCommit.trim()) { + console.log(`[DEBUG] Verified: GitHub ai-dev branch matches master branch`); + } else { + console.log(`[WARN] GitHub ai-dev branch does not match master branch!`); + console.log(`[WARN] master: ${masterCommit.trim()}, GitHub ai-dev: ${githubAiDevHash}`); + } + } catch (error) { + console.error(`[ERROR] Force push to GitHub failed: ${error?.message}`); + throw error; + } + } + + // Final verification + console.log(`[DEBUG] Performing final verification...`); + + // Get master commit hash again to ensure it hasn't changed + const { stdout: finalMasterCommit } = await exec(`git rev-parse master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Final master branch commit hash: ${finalMasterCommit.trim()}`); + + // Get ai-dev commit hash + const { stdout: finalAiDevCommit } = await exec(`git rev-parse ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Final ai-dev branch commit hash: ${finalAiDevCommit.trim()}`); + + // Get remote branches + const { stdout: finalRemoteBranches } = await exec(`git ls-remote --heads`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Final remote branches: ${finalRemoteBranches}`); + + if (finalAiDevCommit.trim() !== finalMasterCommit.trim()) { + console.error(`[ERROR] Final verification failed: ai-dev and master branches point to different commits!`); + console.error(`[ERROR] master: ${finalMasterCommit.trim()}, ai-dev: ${finalAiDevCommit.trim()}`); + } else { + console.log(`[DEBUG] Final verification passed: ai-dev and master branches point to the same commit`); + } + + console.log(`[DEBUG] Reset of ai-dev branch completed successfully`); + return { message: "Branch ai-dev has been reset to be an exact copy of master" }; + } catch (error) { + console.error(`[ERROR] Error during reset of dev branch: ${error?.message}`); + throw new Error(`Error during reset of dev branch: ${error?.message}`); + } + } + + static async _resetDevBranchGitHub() { + try { + console.log('[DEBUG] Switching to branch "master" (GitHub)...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log('[DEBUG] Resetting branch "ai-dev" to be identical to "master" (GitHub)...'); + await exec(`git checkout -B ai-dev master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log('[DEBUG] Pushing updated branch "ai-dev" to remote (GitHub, force push)...'); + await exec(`git push -f github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + return { message: 'ai-dev branch successfully reset to master (GitHub).' }; + } catch (error) { + console.error("Error during resetting ai-dev branch (GitHub):", error?.message); + if (error.stdout) { + console.error("Reset GitHub stdout:", error.stdout); + } + if (error.stderr) { + console.error("Reset GitHub stderr:", error.stderr); + } + throw new Error(`Error during resetting ai-dev branch (GitHub): ${error?.message}`); + } + } + + static async _pushChangesToGitea() { + try { + const { stdout, stderr } = await exec(`git push gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (stdout) { + console.log("Git push Gitea stdout:", stdout); + } + if (stderr) { + console.error("Git push Gitea stderr:", stderr); + } + return { message: "Changes pushed to Gitea remote repository (ai-dev branch)" }; + } catch (error) { + console.error("Git push Gitea error:", error?.message); + if (error.stdout) { + console.error("Git push Gitea stdout:", error.stdout); + } + if (error.stderr) { + console.error("Git push Gitea stderr:", error.stderr); + } + throw error; + } + } + + static async _pushChangesToGithub() { + try { + const { stdout, stderr } = await exec(`git push github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (stdout) { + console.log("Git push GitHub stdout:", stdout); + } + if (stderr) { + console.error("Git push GitHub stderr:", stderr); + } + return { message: "Changes pushed to GitHub repository (ai-dev branch)" }; + } catch (error) { + console.error("Git push GitHub error:", error?.message); + if (error.stdout) { + console.error("Git push GitHub stdout:", error.stdout); + } + if (error.stderr) { + console.error("Git push GitHub stderr:", error.stderr); + } + throw error; + } + } + + static async _addGithubRemote() { + if (GITHUB_REPO_URL) { + try { + const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (!remotes.includes('github')) { + console.log(`[DEBUG] Adding GitHub remote: git remote add github ${GITHUB_REPO_URL}`); + await exec(`git remote add github ${GITHUB_REPO_URL}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub remote added: ${GITHUB_REPO_URL}`); + } else { + console.log(`[DEBUG] GitHub remote already exists.`); + } + } catch (error) { + console.error(`[ERROR] Failed to add GitHub remote: ${error?.message}`); + if (error.stdout) { + console.error(`[ERROR] git remote add stdout: ${error.stdout}`); + } + if (error.stderr) { + console.error(`[ERROR] git remote add stderr: ${error.stderr}`); + } + throw error; + } + } + } + + static async _addGiteaRemote(giteaRemoteUrl) { + try { + const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (!remotes.includes('gitea')) { + console.log(`[DEBUG] Adding Gitea remote: git remote add gitea ${giteaRemoteUrl}`); + await exec(`git remote add gitea ${giteaRemoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea remote added: ${giteaRemoteUrl}`); + } else { + console.log(`[DEBUG] Gitea remote already exists.`); + } + } catch (error) { + console.error(`[ERROR] Failed to add Gitea remote: ${error?.message}`); + if (error.stdout) { + console.error(`[ERROR] git remote add stdout: ${error.stdout}`); + } + if (error.stderr) { + console.error(`[ERROR] git remote add stderr: ${error.stderr}`); + } + throw error; + } + } + + static async _revertGitHubChanges(branchName) { + try { + await exec(`git push -f github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (error) { + console.error("Error during revertGitHubChanges:", error?.message); + if (error.stdout) { + console.error("revertGitHubChanges stdout:", error.stdout); + } + if (error.stderr) { + console.error("revertGitHubChanges stderr:", error.stderr); + } + throw new Error(`Error during revertGitHubChanges: ${error?.message}`); + } + } + + static async _ensureDevBranch() { + try { + console.log(`[DEBUG] Ensuring we are on 'ai-dev' branch...`); + + const { stdout: branchList } = await exec(`git branch --list ai-dev`, { + cwd: ROOT_PATH, + maxBuffer: MAX_BUFFER, + }); + + if (!branchList || branchList.trim() === '') { + console.log(`[DEBUG] Branch 'ai-dev' not found. Creating branch 'ai-dev'.`); + await exec(`git checkout -b ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + const { stdout: currentBranchStdout } = await exec(`git rev-parse --abbrev-ref HEAD`, { + cwd: ROOT_PATH, + maxBuffer: MAX_BUFFER, + }); + const currentBranch = currentBranchStdout.trim(); + + if (currentBranch !== 'ai-dev') { + console.log(`[DEBUG] Switching from branch '${currentBranch}' to 'ai-dev'.`); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + console.log(`[DEBUG] Already on branch 'ai-dev'.`); + } + } + + console.log(`[DEBUG] Successfully ensured we are on 'ai-dev' branch.`); + } catch (error) { + console.error(`[ERROR] Error ensuring branch 'ai-dev': ${error?.message}`); + if (error.stdout) { + console.error(`[ERROR] stdout: ${error.stdout}`); + } + if (error.stderr) { + console.error(`[ERROR] stderr: ${error.stderr}`); + } + throw new Error(`Error ensuring branch 'ai-dev': ${error?.message}`); + } + } + + static async _ensureGitignore() { + try { + console.log(`[DEBUG] Checking .gitignore file...`); + const gitignorePath = path.join(ROOT_PATH, '.gitignore'); + + let gitignoreContent = ''; + try { + gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); + console.log(`[DEBUG] Existing .gitignore found.`); + } catch (error) { + console.log(`[DEBUG] .gitignore file not found, creating new one.`); + } + + + const requiredPatterns = [ + 'node_modules/', + '*/node_modules/', + '**/node_modules/', + '*/build/', + '**/build/', + '.DS_Store', + '.env' + ]; + + let needsUpdate = false; + for (const pattern of requiredPatterns) { + if (!gitignoreContent.includes(pattern)) { + gitignoreContent += `\n${pattern}`; + needsUpdate = true; + } + } + + if (needsUpdate) { + console.log(`[DEBUG] Updating .gitignore file with missing patterns.`); + await fs.writeFile(gitignorePath, gitignoreContent.trim(), 'utf8'); + console.log(`[DEBUG] .gitignore file updated successfully.`); + } else { + console.log(`[DEBUG] .gitignore file is up to date.`); + } + + return true; + } catch (error) { + console.error(`[ERROR] Error ensuring .gitignore: ${error?.message}`); + return false; + } + } + + static async _waitForGitLockRelease(gitDir, timeout = 10000, interval = 500) { + const lockFilePath = path.join(gitDir, 'index.lock'); + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + try { + await fs.access(lockFilePath); + console.log('[DEBUG] index.lock file exists. Waiting...'); + await new Promise(resolve => setTimeout(resolve, interval)); + } catch (err) { + console.log('[DEBUG] index.lock file no longer exists. Proceeding...'); + return; + } + } + + throw new Error('Timeout waiting for index.lock to be released'); + } + + static async _removeGitLockIfExists(gitDir) { + const lockFilePath = path.join(gitDir, 'index.lock'); + try { + await fs.access(lockFilePath); + console.log('[DEBUG] index.lock file exists. Removing...'); + await fs.unlink(lockFilePath); + console.log('[DEBUG] index.lock file removed.'); + } catch (err) { + console.log('[DEBUG] index.lock file does not exist. No action needed.'); + } + } + + static async _saveDevSchema(commitMessage, dev_schema) { + try { + let devSchemaData = {}; + try { + const fileContent = await fs.readFile(devSchemaFilePath, 'utf8'); + devSchemaData = JSON.parse(fileContent); + } catch (readError) { + console.log(`[DEBUG] _dev_schema.json not found or empty, creating new.`); + devSchemaData = {}; + } + + const schema = JSON.parse(dev_schema); + + devSchemaData[commitMessage] = JSON.stringify(schema); + + await fs.writeFile(devSchemaFilePath, JSON.stringify(devSchemaData, null, 2), 'utf8'); + console.log(`[DEBUG] _dev_schema.json updated with new schema for commit '${commitMessage}'`); + } catch (error) { + console.error(`[ERROR] Error saving dev schema: ${error?.message}`); + throw new Error(`Error saving dev schema: ${error?.message}`); + } + } + + static async _getDevSchemaByHash(hash) { + try { + const fileContent = await fs.readFile(devSchemaFilePath, 'utf8'); + const devSchemaData = JSON.parse(fileContent); + + if (devSchemaData[hash]) { + return devSchemaData[hash]; + } else { + throw new Error(`Schema not found for commit hash: ${hash}`); + } + } catch (error) { + console.error(`[ERROR] Error reading dev schema: ${error?.message}`); + console.error(`Error reading dev schema: ${error?.message}`); + } + } + + static async _getDevSchemaByCommitMessage(commitMessage) { + try { + const fileContent = await fs.readFile(devSchemaFilePath, 'utf8'); + const devSchemaData = JSON.parse(fileContent); + + if (devSchemaData[commitMessage]) { + return devSchemaData[commitMessage]; + } else { + throw new Error(`Schema not found for commit message: ${commitMessage}`); + } + } catch (error) { + console.error(`[ERROR] Error retrieving dev schema: ${error.message}`); + throw new Error(`Error retrieving dev schema: ${error.message}`); + } + } + + static async _getCommitMessageByHash(commitHash) { + return new Promise((resolve, reject) => { + exec(`git log -1 --format=%B ${commitHash}`, (error, stdout, stderr) => { + if (error) { + reject(`Error getting commit message: ${stderr}`); + } else { + resolve(stdout.trim()); + } + }); + }); + } +} + +module.exports = VCS; \ No newline at end of file diff --git a/app-shell/yarn.lock b/app-shell/yarn.lock new file mode 100644 index 0000000..63ccb71 --- /dev/null +++ b/app-shell/yarn.lock @@ -0,0 +1,3044 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@one-ini/wasm@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" + integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +accepts@^1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.1, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array.prototype.map@^1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.7.tgz#82fa4d6027272d1fca28a63bbda424d0185d78a7" + integrity sha512-XpcFfLoBEAhezrrNw1V+yLXkE7M6uR7xJEsxbG6c/V9v043qurwVJB9r9UTnoSioFDoz1i1VOydpWGmJpfVZbg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axios@^1.6.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64url@3.x.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +bcrypt@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg== + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +cli-color@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.4.tgz#d658080290968816b322248b7306fad2346fb2c8" + integrity sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA== + dependencies: + d "^1.0.1" + es5-ext "^0.10.64" + es6-iterator "^2.0.3" + memoizee "^0.4.15" + timers-ext "^0.1.7" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-env@7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +debug@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg== + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + +diff@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +editorconfig@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.4.tgz#040c9a8e9a6c5288388b87c2db07028aa89f53a3" + integrity sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q== + dependencies: + "@one-ini/wasm" "0.1.1" + commander "^10.0.0" + minimatch "9.0.1" + semver "^7.5.3" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-abstract@^1.17.0-next.1, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== + dependencies: + is-buffer "~2.0.3" + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2, fresh@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +glob-parent@~5.1.0, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^10.3.3: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.0, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +helmet@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.1.1.tgz#751f0e273d809ace9c172073e0003bed27d27a4a" + integrity sha512-Avg4XxSBrehD94mkRwEljnO+6RZx7AGfk8Wa6K1nxaU+hbXlFOhlOIMgPfFqOYQB/dBCsTpootTGuiOG+CHiQA== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterate-iterator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" + integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== + +iterate-value@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== + dependencies: + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +js-beautify@^1.14.5: + version "1.15.1" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.1.tgz#4695afb508c324e1084ee0b952a102023fc65b64" + integrity sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA== + dependencies: + config-chain "^1.1.13" + editorconfig "^1.0.4" + glob "^10.3.3" + js-cookie "^3.0.5" + nopt "^7.2.0" + +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + +js-yaml@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json2csv@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-5.0.7.tgz#f3a583c25abd9804be873e495d1e65ad8d1b54ae" + integrity sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA== + dependencies: + commander "^6.1.0" + jsonparse "^1.3.1" + lodash.get "^4.4.2" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +jsonwebtoken@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@4.17.21, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memoizee@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949" + integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA== + dependencies: + d "^1.0.2" + es5-ext "^0.10.64" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-descriptors@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.3.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.4.2" + debug "4.1.1" + diff "4.0.2" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.1.6" + growl "1.10.5" + he "1.2.0" + js-yaml "3.14.0" + log-symbols "4.0.0" + minimatch "3.0.4" + ms "2.1.2" + object.assign "4.1.0" + promise.allsettled "1.0.2" + serialize-javascript "4.0.0" + strip-json-comments "3.0.1" + supports-color "7.1.0" + which "2.0.2" + wide-align "1.1.3" + workerpool "6.0.0" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.1" + +moment@2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c" + integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-mocks-http@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.9.0.tgz#6000c570fc4b809603782309be81c73a71d85b71" + integrity sha512-ILf7Ws8xyX9Rl2fLZ7xhZBovrRwgaP84M13esndP6V17M/8j25TpwNzb7Im8U9XCo6fRhdwqiQajWXpsas/E6w== + dependencies: + accepts "^1.3.7" + depd "^1.1.0" + fresh "^0.5.2" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mime "^1.3.4" + parseurl "^1.3.3" + range-parser "^1.2.0" + type-is "^1.6.18" + +nodemon@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.7.tgz#07cb1f455f8bece6a499e0d72b5e029485521a54" + integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@^7.2.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== + dependencies: + abbrev "^2.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +oauth@0.10.x: + version "0.10.0" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58" + integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== + +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-keys@^1.0.11, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +on-finished@2.4.1, on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parseurl@^1.3.3, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +passport-google-oauth2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz#fc9ea59e7091f02e24fd16d6be9257ea982ebbc3" + integrity sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ== + dependencies: + passport-oauth2 "^1.1.2" + +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-microsoft@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/passport-microsoft/-/passport-microsoft-0.1.0.tgz#dc72c1a38b294d74f4dc55fe93f52e25cb9aa5b4" + integrity sha512-0giBDgE1fnR5X84zJZkQ11hnKVrzEgViwRO6RGsormK9zTxFQmN/UHMTDbIpvhk989VqALewB6Pk1R5vNr3GHw== + dependencies: + passport-oauth2 "1.2.0" + pkginfo "0.2.x" + +passport-oauth2@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.2.0.tgz#49613a3eca85c7a1e65bf1019e2b6b80a10c8ac2" + integrity sha512-6128N+n/MOrJdXxdC2q/PVKXtqgihGFIeup+9bsPybAvMPOUKqdGhh9ZIzZF8rFKJOlxUP9fgP3H0JQe18n0rg== + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + +passport-oauth2@^1.1.2: + version "1.8.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8" + integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== + dependencies: + base64url "3.x.x" + oauth "0.10.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkginfo@0.2.x: + version "0.2.3" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" + integrity sha512-7W7wTrE/NsY8xv/DTGjwNIyNah81EQH0MWcTzrHL6pOpMocOGZc0Mbdz9aXxSrp+U0mSmkU8jrNCDCfUs3sOBg== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise.allsettled@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" + integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== + dependencies: + array.prototype.map "^1.0.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + iterate-value "^1.0.0" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.0, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexp.prototype.flags@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" + integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +sequelize-cli@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-6.6.2.tgz#8d838b25c988cf136914cdc3843e19d88c3dcb67" + integrity sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg== + dependencies: + cli-color "^2.0.3" + fs-extra "^9.1.0" + js-beautify "^1.14.5" + lodash "^4.17.21" + resolve "^1.22.1" + umzug "^2.3.0" + yargs "^16.2.0" + +sequelize-json-schema@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sequelize-json-schema/-/sequelize-json-schema-2.1.1.tgz#a82d3813925e81485d76ce291f4ff5c8cb2ae492" + integrity sha512-yCGaHnmQQeL6MQ/fOxhkR5C2aOGZyTD6OrgjP4yw1rbuujuIUVdzWN3AsC6r6AvlGZ3EUBBbCJHKl8OIFFES4Q== + +serialize-javascript@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +supports-color@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +timers-ext@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== + dependencies: + es5-ext "^0.10.64" + next-tick "^1.1.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +type-is@^1.6.18, type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +uid2@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" + integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== + +umzug@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/umzug/-/umzug-2.3.0.tgz#0ef42b62df54e216b05dcaf627830a6a8b84a184" + integrity sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw== + dependencies: + bluebird "^3.7.2" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +workerpool@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" + integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^15.0.1: + version "15.0.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.3.tgz#316e263d5febe8b38eef61ac092b33dfcc9b1115" + integrity sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" + integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== + dependencies: + camelcase "^5.3.1" + decamelize "^1.2.0" + flat "^4.1.0" + is-plain-obj "^1.1.0" + yargs "^14.2.3" + +yargs@13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yargs@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" + integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== + dependencies: + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.1" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/backend/.sequelizerc b/backend/.sequelizerc new file mode 100644 index 0000000..fe89188 --- /dev/null +++ b/backend/.sequelizerc @@ -0,0 +1,7 @@ +const path = require('path'); +module.exports = { + "config": path.resolve("src", "db", "db.config.js"), + "models-path": path.resolve("src", "db", "models"), + "seeders-path": path.resolve("src", "db", "seeders"), + "migrations-path": path.resolve("src", "db", "migrations") +}; \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..581cb98 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +FROM node:20.15.1-alpine + +RUN apk update && apk add bash +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + + +# Bundle app source +COPY . . + + +EXPOSE 8080 + +CMD [ "yarn", "start" ] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..7870249 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,56 @@ + +#meng-leap-cash - template backend, + +#### Run App on local machine: + +##### Install local dependencies: +- `yarn install` + +------------ + +##### Adjust local db: +###### 1. Install postgres: + - MacOS: + - `brew install postgres` + +- Ubuntu: + - `sudo apt update` + - `sudo apt install postgresql postgresql-contrib` + +###### 2. Create db and admin user: + - Before run and test connection, make sure you have created a database as described in the above configuration. You can use the `psql` command to create a user and database. + - `psql postgres --u postgres` + +- Next, type this command for creating a new user with password then give access for creating the database. + - `postgres-# CREATE ROLE admin WITH LOGIN PASSWORD 'admin_pass';` + - `postgres-# ALTER ROLE admin CREATEDB;` + +- Quit `psql` then log in again using the new user that previously created. + - `postgres-# \q` + - `psql postgres -U admin` + +- Type this command to creating a new database. + - `postgres=> CREATE DATABASE db_meng_leap_cash;` + +- Then give that new user privileges to the new database then quit the `psql`. + - `postgres=> GRANT ALL PRIVILEGES ON DATABASE db_meng_leap_cash TO admin;` + - `postgres=> \q` + +------------ + +#### Api Documentation (Swagger) + +http://localhost:8080/api-docs (local host) + +http://host_name/api-docs + +------------ + + ##### Setup database tables or update after schema change + - `yarn db:migrate` + + ##### Seed the initial data (admin accounts, relevant for the first setup): + - `yarn db:seed` + + ##### Start build: + - `yarn start` diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..ab30329 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "mengleapcash", + "description": "meng-leap-cash - template backend", + "scripts": { + "start": "npm run db:migrate && npm run db:seed && npm run watch", + "db:migrate": "sequelize-cli db:migrate", + "db:seed": "sequelize-cli db:seed:all", + "db:drop": "sequelize-cli db:drop", + "db:create": "sequelize-cli db:create", + "watch": "node watcher.js" + }, + "dependencies": { + "@google-cloud/storage": "^5.18.2", + "axios": "^1.6.7", + "bcrypt": "5.1.1", + "chokidar": "^4.0.3", + "cors": "2.8.5", + "csv-parser": "^3.0.0", + "express": "4.18.2", + "formidable": "1.2.2", + "helmet": "4.1.1", + "json2csv": "^5.0.7", + "jsonwebtoken": "8.5.1", + "lodash": "4.17.21", + "moment": "2.30.1", + "multer": "^1.4.4", + "mysql2": "2.2.5", + "nodemailer": "6.9.9", + "passport": "^0.7.0", + "passport-google-oauth2": "^0.2.0", + "passport-jwt": "^4.0.1", + "passport-microsoft": "^0.1.0", + "pg": "8.4.1", + "pg-hstore": "2.3.4", + "sequelize": "6.35.2", + "sequelize-json-schema": "^2.1.1", + "sqlite": "4.0.15", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "tedious": "^18.2.4" + }, + "engines": { + "node": ">=18" + }, + "private": true, + "devDependencies": { + "cross-env": "7.0.3", + "mocha": "8.1.3", + "node-mocks-http": "1.9.0", + "nodemon": "2.0.5", + "sequelize-cli": "6.6.2" + } +} diff --git a/backend/src/auth/auth.js b/backend/src/auth/auth.js new file mode 100644 index 0000000..5cbb79a --- /dev/null +++ b/backend/src/auth/auth.js @@ -0,0 +1,27 @@ +const config = require('../config'); + +const passport = require('passport'); +const JWTstrategy = require('passport-jwt').Strategy; +const ExtractJWT = require('passport-jwt').ExtractJwt; +const UsersDBApi = require('../db/api/users'); + + +passport.use(new JWTstrategy({ + passReqToCallback: true, + secretOrKey: config.secret_key, + jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken() +}, async (req, token, done) => { + try { + const user = await UsersDBApi.findBy( {email: token.user.email}); + + if (user && user.disabled) { + return done (new Error(`User '${user.email}' is disabled`)); + } + + req.currentUser = user; + + return done(null, user); + } catch (error) { + done(error); + } +})); \ No newline at end of file diff --git a/backend/src/config.js b/backend/src/config.js new file mode 100644 index 0000000..d91e201 --- /dev/null +++ b/backend/src/config.js @@ -0,0 +1,59 @@ + + +const os = require('os'); + +const config = { + gcloud: { + bucket: "fldemo-files", + hash: "5bad16aa22cf5ea65fe7a463302d6865" + }, + bcrypt: { + saltRounds: 12 + }, + admin_pass: "f087aa9d", + user_pass: "8f5c99381bbf", + admin_email: "admin@flatlogic.com", + providers: { + LOCAL: 'local', + GOOGLE: 'google', + MICROSOFT: 'microsoft' + }, + secret_key: process.env.SECRET_KEY || '', + remote: '', + port: process.env.NODE_ENV === "production" ? "" : "8080", + hostUI: process.env.NODE_ENV === "production" ? "" : "http://localhost", + portUI: process.env.NODE_ENV === "production" ? "" : "3000", + portUIProd: process.env.NODE_ENV === "production" ? "" : ":3000", + swaggerUI: process.env.NODE_ENV === "production" ? "" : "http://localhost", + swaggerPort: process.env.NODE_ENV === "production" ? "" : ":8080", + + uploadDir: os.tmpdir(), + email: { + from: 'meng-leap-cash ', + host: 'email-smtp.us-east-1.amazonaws.com', + port: 587, + auth: { + user: process.env.EMAIL_USER || '', + pass: process.env.EMAIL_PASS, + }, + tls: { + rejectUnauthorized: false + } + }, + roles: { + admin: 'Administrator', + user: 'User', + }, + + project_uuid: 'f087aa9d-30b0-40b5-857b-8f5c99381bbf', + flHost: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'dev_stage' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects', + +}; + +config.host = process.env.NODE_ENV === "production" ? config.remote : "http://localhost"; +config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`; +config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; +config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; +config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; + +module.exports = config; diff --git a/backend/src/db/api/branches.js b/backend/src/db/api/branches.js new file mode 100644 index 0000000..ea5b41a --- /dev/null +++ b/backend/src/db/api/branches.js @@ -0,0 +1,313 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class BranchesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const branches = await db.branches.create( + { + id: data.id || undefined, + + code: data.code + || + null + , + + name: data.name + || + null + , + + description: data.description + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return branches; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const branchesData = data.map((item, index) => ({ + id: item.id || undefined, + + code: item.code + || + null + , + + name: item.name + || + null + , + + description: item.description + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const branches = await db.branches.bulkCreate(branchesData, { transaction }); + + return branches; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const branches = await db.branches.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.code !== undefined) updatePayload.code = data.code; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.description !== undefined) updatePayload.description = data.description; + + updatePayload.updatedById = currentUser.id; + + await branches.update(updatePayload, {transaction}); + + return branches; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const branches = await db.branches.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of branches) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of branches) { + await record.destroy({transaction}); + } + }); + + return branches; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const branches = await db.branches.findByPk(id, options); + + await branches.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await branches.destroy({ + transaction + }); + + return branches; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const branches = await db.branches.findOne( + { where }, + { transaction }, + ); + + if (!branches) { + return branches; + } + + const output = branches.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.code) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'branches', + 'code', + filter.code, + ), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'branches', + 'name', + filter.name, + ), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'branches', + 'description', + filter.description, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.branches.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'branches', + 'name', + query, + ), + ], + }; + } + + const records = await db.branches.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/calendars.js b/backend/src/db/api/calendars.js new file mode 100644 index 0000000..c6ad0d5 --- /dev/null +++ b/backend/src/db/api/calendars.js @@ -0,0 +1,368 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class CalendarsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const calendars = await db.calendars.create( + { + id: data.id || undefined, + + date: data.date + || + null + , + + is_weekend: data.is_weekend + || + false + + , + + is_holiday: data.is_holiday + || + false + + , + + description: data.description + || + null + , + + flag: data.flag + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return calendars; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const calendarsData = data.map((item, index) => ({ + id: item.id || undefined, + + date: item.date + || + null + , + + is_weekend: item.is_weekend + || + false + + , + + is_holiday: item.is_holiday + || + false + + , + + description: item.description + || + null + , + + flag: item.flag + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const calendars = await db.calendars.bulkCreate(calendarsData, { transaction }); + + return calendars; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const calendars = await db.calendars.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.date !== undefined) updatePayload.date = data.date; + + if (data.is_weekend !== undefined) updatePayload.is_weekend = data.is_weekend; + + if (data.is_holiday !== undefined) updatePayload.is_holiday = data.is_holiday; + + if (data.description !== undefined) updatePayload.description = data.description; + + if (data.flag !== undefined) updatePayload.flag = data.flag; + + updatePayload.updatedById = currentUser.id; + + await calendars.update(updatePayload, {transaction}); + + return calendars; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const calendars = await db.calendars.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of calendars) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of calendars) { + await record.destroy({transaction}); + } + }); + + return calendars; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const calendars = await db.calendars.findByPk(id, options); + + await calendars.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await calendars.destroy({ + transaction + }); + + return calendars; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const calendars = await db.calendars.findOne( + { where }, + { transaction }, + ); + + if (!calendars) { + return calendars; + } + + const output = calendars.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'calendars', + 'description', + filter.description, + ), + }; + } + + if (filter.flag) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'calendars', + 'flag', + filter.flag, + ), + }; + } + + if (filter.dateRange) { + const [start, end] = filter.dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + date: { + ...where.date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + date: { + ...where.date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.is_weekend) { + where = { + ...where, + is_weekend: filter.is_weekend, + }; + } + + if (filter.is_holiday) { + where = { + ...where, + is_holiday: filter.is_holiday, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.calendars.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'calendars', + 'description', + query, + ), + ], + }; + } + + const records = await db.calendars.findAll({ + attributes: [ 'id', 'description' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['description', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.description, + })); + } + +}; + diff --git a/backend/src/db/api/client_status.js b/backend/src/db/api/client_status.js new file mode 100644 index 0000000..39aefb8 --- /dev/null +++ b/backend/src/db/api/client_status.js @@ -0,0 +1,267 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Client_statusDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const client_status = await db.client_status.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return client_status; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const client_statusData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const client_status = await db.client_status.bulkCreate(client_statusData, { transaction }); + + return client_status; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const client_status = await db.client_status.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await client_status.update(updatePayload, {transaction}); + + return client_status; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const client_status = await db.client_status.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of client_status) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of client_status) { + await record.destroy({transaction}); + } + }); + + return client_status; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const client_status = await db.client_status.findByPk(id, options); + + await client_status.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await client_status.destroy({ + transaction + }); + + return client_status; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const client_status = await db.client_status.findOne( + { where }, + { transaction }, + ); + + if (!client_status) { + return client_status; + } + + const output = client_status.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'client_status', + 'name', + filter.name, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.client_status.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'client_status', + 'name', + query, + ), + ], + }; + } + + const records = await db.client_status.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/clients.js b/backend/src/db/api/clients.js new file mode 100644 index 0000000..08d7c7b --- /dev/null +++ b/backend/src/db/api/clients.js @@ -0,0 +1,435 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class ClientsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const clients = await db.clients.create( + { + id: data.id || undefined, + + code: data.code + || + null + , + + name_en: data.name_en + || + null + , + + name_kh: data.name_kh + || + null + , + + date_of_birth: data.date_of_birth + || + null + , + + phone_number: data.phone_number + || + null + , + + is_new: data.is_new + || + false + + , + + document_number: data.document_number + || + null + , + + sex: data.sex + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return clients; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const clientsData = data.map((item, index) => ({ + id: item.id || undefined, + + code: item.code + || + null + , + + name_en: item.name_en + || + null + , + + name_kh: item.name_kh + || + null + , + + date_of_birth: item.date_of_birth + || + null + , + + phone_number: item.phone_number + || + null + , + + is_new: item.is_new + || + false + + , + + document_number: item.document_number + || + null + , + + sex: item.sex + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const clients = await db.clients.bulkCreate(clientsData, { transaction }); + + return clients; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const clients = await db.clients.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.code !== undefined) updatePayload.code = data.code; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.date_of_birth !== undefined) updatePayload.date_of_birth = data.date_of_birth; + + if (data.phone_number !== undefined) updatePayload.phone_number = data.phone_number; + + if (data.is_new !== undefined) updatePayload.is_new = data.is_new; + + if (data.document_number !== undefined) updatePayload.document_number = data.document_number; + + if (data.sex !== undefined) updatePayload.sex = data.sex; + + updatePayload.updatedById = currentUser.id; + + await clients.update(updatePayload, {transaction}); + + return clients; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const clients = await db.clients.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of clients) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of clients) { + await record.destroy({transaction}); + } + }); + + return clients; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const clients = await db.clients.findByPk(id, options); + + await clients.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await clients.destroy({ + transaction + }); + + return clients; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const clients = await db.clients.findOne( + { where }, + { transaction }, + ); + + if (!clients) { + return clients; + } + + const output = clients.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.code) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'clients', + 'code', + filter.code, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'clients', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'clients', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.phone_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'clients', + 'phone_number', + filter.phone_number, + ), + }; + } + + if (filter.document_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'clients', + 'document_number', + filter.document_number, + ), + }; + } + + if (filter.date_of_birthRange) { + const [start, end] = filter.date_of_birthRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.is_new) { + where = { + ...where, + is_new: filter.is_new, + }; + } + + if (filter.sex) { + where = { + ...where, + sex: filter.sex, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.clients.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'clients', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.clients.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/communes.js b/backend/src/db/api/communes.js new file mode 100644 index 0000000..7efa8c9 --- /dev/null +++ b/backend/src/db/api/communes.js @@ -0,0 +1,290 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class CommunesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const communes = await db.communes.create( + { + id: data.id || undefined, + + name_kh: data.name_kh + || + null + , + + name_en: data.name_en + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return communes; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const communesData = data.map((item, index) => ({ + id: item.id || undefined, + + name_kh: item.name_kh + || + null + , + + name_en: item.name_en + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const communes = await db.communes.bulkCreate(communesData, { transaction }); + + return communes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const communes = await db.communes.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + updatePayload.updatedById = currentUser.id; + + await communes.update(updatePayload, {transaction}); + + return communes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const communes = await db.communes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of communes) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of communes) { + await record.destroy({transaction}); + } + }); + + return communes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const communes = await db.communes.findByPk(id, options); + + await communes.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await communes.destroy({ + transaction + }); + + return communes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const communes = await db.communes.findOne( + { where }, + { transaction }, + ); + + if (!communes) { + return communes; + } + + const output = communes.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'communes', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'communes', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.communes.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'communes', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.communes.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/deposits.js b/backend/src/db/api/deposits.js new file mode 100644 index 0000000..19f3bc2 --- /dev/null +++ b/backend/src/db/api/deposits.js @@ -0,0 +1,316 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class DepositsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const deposits = await db.deposits.create( + { + id: data.id || undefined, + + deposit_datetime: data.deposit_datetime + || + null + , + + deposit_amount: data.deposit_amount + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return deposits; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const depositsData = data.map((item, index) => ({ + id: item.id || undefined, + + deposit_datetime: item.deposit_datetime + || + null + , + + deposit_amount: item.deposit_amount + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const deposits = await db.deposits.bulkCreate(depositsData, { transaction }); + + return deposits; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const deposits = await db.deposits.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.deposit_datetime !== undefined) updatePayload.deposit_datetime = data.deposit_datetime; + + if (data.deposit_amount !== undefined) updatePayload.deposit_amount = data.deposit_amount; + + updatePayload.updatedById = currentUser.id; + + await deposits.update(updatePayload, {transaction}); + + return deposits; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const deposits = await db.deposits.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of deposits) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of deposits) { + await record.destroy({transaction}); + } + }); + + return deposits; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const deposits = await db.deposits.findByPk(id, options); + + await deposits.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await deposits.destroy({ + transaction + }); + + return deposits; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const deposits = await db.deposits.findOne( + { where }, + { transaction }, + ); + + if (!deposits) { + return deposits; + } + + const output = deposits.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.deposit_datetimeRange) { + const [start, end] = filter.deposit_datetimeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + deposit_datetime: { + ...where.deposit_datetime, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + deposit_datetime: { + ...where.deposit_datetime, + [Op.lte]: end, + }, + }; + } + } + + if (filter.deposit_amountRange) { + const [start, end] = filter.deposit_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + deposit_amount: { + ...where.deposit_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + deposit_amount: { + ...where.deposit_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.deposits.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'deposits', + 'deposit_amount', + query, + ), + ], + }; + } + + const records = await db.deposits.findAll({ + attributes: [ 'id', 'deposit_amount' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['deposit_amount', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.deposit_amount, + })); + } + +}; + diff --git a/backend/src/db/api/districts.js b/backend/src/db/api/districts.js new file mode 100644 index 0000000..0e3ac23 --- /dev/null +++ b/backend/src/db/api/districts.js @@ -0,0 +1,290 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class DistrictsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const districts = await db.districts.create( + { + id: data.id || undefined, + + name_kh: data.name_kh + || + null + , + + name_en: data.name_en + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return districts; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const districtsData = data.map((item, index) => ({ + id: item.id || undefined, + + name_kh: item.name_kh + || + null + , + + name_en: item.name_en + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const districts = await db.districts.bulkCreate(districtsData, { transaction }); + + return districts; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const districts = await db.districts.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + updatePayload.updatedById = currentUser.id; + + await districts.update(updatePayload, {transaction}); + + return districts; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const districts = await db.districts.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of districts) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of districts) { + await record.destroy({transaction}); + } + }); + + return districts; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const districts = await db.districts.findByPk(id, options); + + await districts.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await districts.destroy({ + transaction + }); + + return districts; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const districts = await db.districts.findOne( + { where }, + { transaction }, + ); + + if (!districts) { + return districts; + } + + const output = districts.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'districts', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'districts', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.districts.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'districts', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.districts.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/document_type.js b/backend/src/db/api/document_type.js new file mode 100644 index 0000000..f2030eb --- /dev/null +++ b/backend/src/db/api/document_type.js @@ -0,0 +1,267 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Document_typeDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const document_type = await db.document_type.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return document_type; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const document_typeData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const document_type = await db.document_type.bulkCreate(document_typeData, { transaction }); + + return document_type; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const document_type = await db.document_type.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await document_type.update(updatePayload, {transaction}); + + return document_type; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const document_type = await db.document_type.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of document_type) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of document_type) { + await record.destroy({transaction}); + } + }); + + return document_type; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const document_type = await db.document_type.findByPk(id, options); + + await document_type.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await document_type.destroy({ + transaction + }); + + return document_type; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const document_type = await db.document_type.findOne( + { where }, + { transaction }, + ); + + if (!document_type) { + return document_type; + } + + const output = document_type.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'document_type', + 'name', + filter.name, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.document_type.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'document_type', + 'name', + query, + ), + ], + }; + } + + const records = await db.document_type.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/expense_items.js b/backend/src/db/api/expense_items.js new file mode 100644 index 0000000..d46f191 --- /dev/null +++ b/backend/src/db/api/expense_items.js @@ -0,0 +1,339 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Expense_itemsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const expense_items = await db.expense_items.create( + { + id: data.id || undefined, + + expense_datetime: data.expense_datetime + || + null + , + + description: data.description + || + null + , + + expense_amount: data.expense_amount + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return expense_items; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const expense_itemsData = data.map((item, index) => ({ + id: item.id || undefined, + + expense_datetime: item.expense_datetime + || + null + , + + description: item.description + || + null + , + + expense_amount: item.expense_amount + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const expense_items = await db.expense_items.bulkCreate(expense_itemsData, { transaction }); + + return expense_items; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const expense_items = await db.expense_items.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.expense_datetime !== undefined) updatePayload.expense_datetime = data.expense_datetime; + + if (data.description !== undefined) updatePayload.description = data.description; + + if (data.expense_amount !== undefined) updatePayload.expense_amount = data.expense_amount; + + updatePayload.updatedById = currentUser.id; + + await expense_items.update(updatePayload, {transaction}); + + return expense_items; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const expense_items = await db.expense_items.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of expense_items) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of expense_items) { + await record.destroy({transaction}); + } + }); + + return expense_items; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const expense_items = await db.expense_items.findByPk(id, options); + + await expense_items.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await expense_items.destroy({ + transaction + }); + + return expense_items; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const expense_items = await db.expense_items.findOne( + { where }, + { transaction }, + ); + + if (!expense_items) { + return expense_items; + } + + const output = expense_items.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'expense_items', + 'description', + filter.description, + ), + }; + } + + if (filter.expense_datetimeRange) { + const [start, end] = filter.expense_datetimeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + expense_datetime: { + ...where.expense_datetime, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + expense_datetime: { + ...where.expense_datetime, + [Op.lte]: end, + }, + }; + } + } + + if (filter.expense_amountRange) { + const [start, end] = filter.expense_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + expense_amount: { + ...where.expense_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + expense_amount: { + ...where.expense_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.expense_items.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'expense_items', + 'description', + query, + ), + ], + }; + } + + const records = await db.expense_items.findAll({ + attributes: [ 'id', 'description' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['description', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.description, + })); + } + +}; + diff --git a/backend/src/db/api/expense_types.js b/backend/src/db/api/expense_types.js new file mode 100644 index 0000000..88997fc --- /dev/null +++ b/backend/src/db/api/expense_types.js @@ -0,0 +1,290 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Expense_typesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const expense_types = await db.expense_types.create( + { + id: data.id || undefined, + + name_kh: data.name_kh + || + null + , + + name_en: data.name_en + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return expense_types; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const expense_typesData = data.map((item, index) => ({ + id: item.id || undefined, + + name_kh: item.name_kh + || + null + , + + name_en: item.name_en + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const expense_types = await db.expense_types.bulkCreate(expense_typesData, { transaction }); + + return expense_types; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const expense_types = await db.expense_types.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + updatePayload.updatedById = currentUser.id; + + await expense_types.update(updatePayload, {transaction}); + + return expense_types; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const expense_types = await db.expense_types.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of expense_types) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of expense_types) { + await record.destroy({transaction}); + } + }); + + return expense_types; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const expense_types = await db.expense_types.findByPk(id, options); + + await expense_types.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await expense_types.destroy({ + transaction + }); + + return expense_types; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const expense_types = await db.expense_types.findOne( + { where }, + { transaction }, + ); + + if (!expense_types) { + return expense_types; + } + + const output = expense_types.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'expense_types', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'expense_types', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.expense_types.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'expense_types', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.expense_types.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/group_menus.js b/backend/src/db/api/group_menus.js new file mode 100644 index 0000000..dcbacbc --- /dev/null +++ b/backend/src/db/api/group_menus.js @@ -0,0 +1,288 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Group_menusDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const group_menus = await db.group_menus.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + is_admin: data.is_admin + || + false + + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return group_menus; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const group_menusData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + is_admin: item.is_admin + || + false + + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const group_menus = await db.group_menus.bulkCreate(group_menusData, { transaction }); + + return group_menus; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const group_menus = await db.group_menus.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.is_admin !== undefined) updatePayload.is_admin = data.is_admin; + + updatePayload.updatedById = currentUser.id; + + await group_menus.update(updatePayload, {transaction}); + + return group_menus; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const group_menus = await db.group_menus.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of group_menus) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of group_menus) { + await record.destroy({transaction}); + } + }); + + return group_menus; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const group_menus = await db.group_menus.findByPk(id, options); + + await group_menus.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await group_menus.destroy({ + transaction + }); + + return group_menus; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const group_menus = await db.group_menus.findOne( + { where }, + { transaction }, + ); + + if (!group_menus) { + return group_menus; + } + + const output = group_menus.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'group_menus', + 'name', + filter.name, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.is_admin) { + where = { + ...where, + is_admin: filter.is_admin, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.group_menus.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'group_menus', + 'name', + query, + ), + ], + }; + } + + const records = await db.group_menus.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/guarantor.js b/backend/src/db/api/guarantor.js new file mode 100644 index 0000000..e612f01 --- /dev/null +++ b/backend/src/db/api/guarantor.js @@ -0,0 +1,414 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class GuarantorDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const guarantor = await db.guarantor.create( + { + id: data.id || undefined, + + full_name: data.full_name + || + null + , + + sex: data.sex + || + null + , + + date_of_birth: data.date_of_birth + || + null + , + + document_type: data.document_type + || + null + , + + document_number: data.document_number + || + null + , + + phone_number: data.phone_number + || + null + , + + full_address_input: data.full_address_input + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return guarantor; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const guarantorData = data.map((item, index) => ({ + id: item.id || undefined, + + full_name: item.full_name + || + null + , + + sex: item.sex + || + null + , + + date_of_birth: item.date_of_birth + || + null + , + + document_type: item.document_type + || + null + , + + document_number: item.document_number + || + null + , + + phone_number: item.phone_number + || + null + , + + full_address_input: item.full_address_input + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const guarantor = await db.guarantor.bulkCreate(guarantorData, { transaction }); + + return guarantor; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const guarantor = await db.guarantor.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.full_name !== undefined) updatePayload.full_name = data.full_name; + + if (data.sex !== undefined) updatePayload.sex = data.sex; + + if (data.date_of_birth !== undefined) updatePayload.date_of_birth = data.date_of_birth; + + if (data.document_type !== undefined) updatePayload.document_type = data.document_type; + + if (data.document_number !== undefined) updatePayload.document_number = data.document_number; + + if (data.phone_number !== undefined) updatePayload.phone_number = data.phone_number; + + if (data.full_address_input !== undefined) updatePayload.full_address_input = data.full_address_input; + + updatePayload.updatedById = currentUser.id; + + await guarantor.update(updatePayload, {transaction}); + + return guarantor; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const guarantor = await db.guarantor.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of guarantor) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of guarantor) { + await record.destroy({transaction}); + } + }); + + return guarantor; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const guarantor = await db.guarantor.findByPk(id, options); + + await guarantor.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await guarantor.destroy({ + transaction + }); + + return guarantor; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const guarantor = await db.guarantor.findOne( + { where }, + { transaction }, + ); + + if (!guarantor) { + return guarantor; + } + + const output = guarantor.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.full_name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'guarantor', + 'full_name', + filter.full_name, + ), + }; + } + + if (filter.document_type) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'guarantor', + 'document_type', + filter.document_type, + ), + }; + } + + if (filter.document_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'guarantor', + 'document_number', + filter.document_number, + ), + }; + } + + if (filter.phone_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'guarantor', + 'phone_number', + filter.phone_number, + ), + }; + } + + if (filter.full_address_input) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'guarantor', + 'full_address_input', + filter.full_address_input, + ), + }; + } + + if (filter.date_of_birthRange) { + const [start, end] = filter.date_of_birthRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.sex) { + where = { + ...where, + sex: filter.sex, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.guarantor.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'guarantor', + 'full_name', + query, + ), + ], + }; + } + + const records = await db.guarantor.findAll({ + attributes: [ 'id', 'full_name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['full_name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.full_name, + })); + } + +}; + diff --git a/backend/src/db/api/interest_rates.js b/backend/src/db/api/interest_rates.js new file mode 100644 index 0000000..2b8cda5 --- /dev/null +++ b/backend/src/db/api/interest_rates.js @@ -0,0 +1,480 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Interest_ratesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const interest_rates = await db.interest_rates.create( + { + id: data.id || undefined, + + code: data.code + || + null + , + + name: data.name + || + null + , + + rate: data.rate + || + null + , + + commission_rate: data.commission_rate + || + null + , + + interval: data.interval + || + null + , + + sort: data.sort + || + null + , + + css: data.css + || + null + , + + setting: data.setting + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return interest_rates; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const interest_ratesData = data.map((item, index) => ({ + id: item.id || undefined, + + code: item.code + || + null + , + + name: item.name + || + null + , + + rate: item.rate + || + null + , + + commission_rate: item.commission_rate + || + null + , + + interval: item.interval + || + null + , + + sort: item.sort + || + null + , + + css: item.css + || + null + , + + setting: item.setting + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const interest_rates = await db.interest_rates.bulkCreate(interest_ratesData, { transaction }); + + return interest_rates; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const interest_rates = await db.interest_rates.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.code !== undefined) updatePayload.code = data.code; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.rate !== undefined) updatePayload.rate = data.rate; + + if (data.commission_rate !== undefined) updatePayload.commission_rate = data.commission_rate; + + if (data.interval !== undefined) updatePayload.interval = data.interval; + + if (data.sort !== undefined) updatePayload.sort = data.sort; + + if (data.css !== undefined) updatePayload.css = data.css; + + if (data.setting !== undefined) updatePayload.setting = data.setting; + + updatePayload.updatedById = currentUser.id; + + await interest_rates.update(updatePayload, {transaction}); + + return interest_rates; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const interest_rates = await db.interest_rates.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of interest_rates) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of interest_rates) { + await record.destroy({transaction}); + } + }); + + return interest_rates; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const interest_rates = await db.interest_rates.findByPk(id, options); + + await interest_rates.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await interest_rates.destroy({ + transaction + }); + + return interest_rates; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const interest_rates = await db.interest_rates.findOne( + { where }, + { transaction }, + ); + + if (!interest_rates) { + return interest_rates; + } + + const output = interest_rates.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.code) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'interest_rates', + 'code', + filter.code, + ), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'interest_rates', + 'name', + filter.name, + ), + }; + } + + if (filter.css) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'interest_rates', + 'css', + filter.css, + ), + }; + } + + if (filter.setting) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'interest_rates', + 'setting', + filter.setting, + ), + }; + } + + if (filter.rateRange) { + const [start, end] = filter.rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + rate: { + ...where.rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + rate: { + ...where.rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.commission_rateRange) { + const [start, end] = filter.commission_rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + commission_rate: { + ...where.commission_rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + commission_rate: { + ...where.commission_rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.intervalRange) { + const [start, end] = filter.intervalRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + interval: { + ...where.interval, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + interval: { + ...where.interval, + [Op.lte]: end, + }, + }; + } + } + + if (filter.sortRange) { + const [start, end] = filter.sortRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + sort: { + ...where.sort, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + sort: { + ...where.sort, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.interest_rates.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'interest_rates', + 'name', + query, + ), + ], + }; + } + + const records = await db.interest_rates.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/loan_status.js b/backend/src/db/api/loan_status.js new file mode 100644 index 0000000..98bee27 --- /dev/null +++ b/backend/src/db/api/loan_status.js @@ -0,0 +1,290 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Loan_statusDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loan_status = await db.loan_status.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + css: data.css + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return loan_status; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const loan_statusData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + css: item.css + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const loan_status = await db.loan_status.bulkCreate(loan_statusData, { transaction }); + + return loan_status; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const loan_status = await db.loan_status.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.css !== undefined) updatePayload.css = data.css; + + updatePayload.updatedById = currentUser.id; + + await loan_status.update(updatePayload, {transaction}); + + return loan_status; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loan_status = await db.loan_status.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of loan_status) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of loan_status) { + await record.destroy({transaction}); + } + }); + + return loan_status; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const loan_status = await db.loan_status.findByPk(id, options); + + await loan_status.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await loan_status.destroy({ + transaction + }); + + return loan_status; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const loan_status = await db.loan_status.findOne( + { where }, + { transaction }, + ); + + if (!loan_status) { + return loan_status; + } + + const output = loan_status.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loan_status', + 'name', + filter.name, + ), + }; + } + + if (filter.css) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loan_status', + 'css', + filter.css, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.loan_status.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'loan_status', + 'name', + query, + ), + ], + }; + } + + const records = await db.loan_status.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/loan_types.js b/backend/src/db/api/loan_types.js new file mode 100644 index 0000000..b9369ef --- /dev/null +++ b/backend/src/db/api/loan_types.js @@ -0,0 +1,290 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Loan_typesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loan_types = await db.loan_types.create( + { + id: data.id || undefined, + + name_kh: data.name_kh + || + null + , + + name_en: data.name_en + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return loan_types; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const loan_typesData = data.map((item, index) => ({ + id: item.id || undefined, + + name_kh: item.name_kh + || + null + , + + name_en: item.name_en + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const loan_types = await db.loan_types.bulkCreate(loan_typesData, { transaction }); + + return loan_types; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const loan_types = await db.loan_types.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + updatePayload.updatedById = currentUser.id; + + await loan_types.update(updatePayload, {transaction}); + + return loan_types; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loan_types = await db.loan_types.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of loan_types) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of loan_types) { + await record.destroy({transaction}); + } + }); + + return loan_types; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const loan_types = await db.loan_types.findByPk(id, options); + + await loan_types.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await loan_types.destroy({ + transaction + }); + + return loan_types; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const loan_types = await db.loan_types.findOne( + { where }, + { transaction }, + ); + + if (!loan_types) { + return loan_types; + } + + const output = loan_types.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loan_types', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loan_types', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.loan_types.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'loan_types', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.loan_types.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/loans.js b/backend/src/db/api/loans.js new file mode 100644 index 0000000..55ef8be --- /dev/null +++ b/backend/src/db/api/loans.js @@ -0,0 +1,794 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class LoansDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loans = await db.loans.create( + { + id: data.id || undefined, + + code: data.code + || + null + , + + principal_amount: data.principal_amount + || + null + , + + term: data.term + || + null + , + + pending_amount: data.pending_amount + || + null + , + + last_pending_amount: data.last_pending_amount + || + null + , + + rate: data.rate + || + null + , + + commission_rate: data.commission_rate + || + null + , + + registration_date: data.registration_date + || + null + , + + started_payment_date: data.started_payment_date + || + null + , + + last_payment_date: data.last_payment_date + || + null + , + + finish_payment_date: data.finish_payment_date + || + null + , + + finish_discount: data.finish_discount + || + null + , + + finish_discount_amount: data.finish_discount_amount + || + null + , + + admin_rate: data.admin_rate + || + null + , + + admin_amount: data.admin_amount + || + null + , + + purpose: data.purpose + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return loans; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const loansData = data.map((item, index) => ({ + id: item.id || undefined, + + code: item.code + || + null + , + + principal_amount: item.principal_amount + || + null + , + + term: item.term + || + null + , + + pending_amount: item.pending_amount + || + null + , + + last_pending_amount: item.last_pending_amount + || + null + , + + rate: item.rate + || + null + , + + commission_rate: item.commission_rate + || + null + , + + registration_date: item.registration_date + || + null + , + + started_payment_date: item.started_payment_date + || + null + , + + last_payment_date: item.last_payment_date + || + null + , + + finish_payment_date: item.finish_payment_date + || + null + , + + finish_discount: item.finish_discount + || + null + , + + finish_discount_amount: item.finish_discount_amount + || + null + , + + admin_rate: item.admin_rate + || + null + , + + admin_amount: item.admin_amount + || + null + , + + purpose: item.purpose + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const loans = await db.loans.bulkCreate(loansData, { transaction }); + + return loans; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const loans = await db.loans.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.code !== undefined) updatePayload.code = data.code; + + if (data.principal_amount !== undefined) updatePayload.principal_amount = data.principal_amount; + + if (data.term !== undefined) updatePayload.term = data.term; + + if (data.pending_amount !== undefined) updatePayload.pending_amount = data.pending_amount; + + if (data.last_pending_amount !== undefined) updatePayload.last_pending_amount = data.last_pending_amount; + + if (data.rate !== undefined) updatePayload.rate = data.rate; + + if (data.commission_rate !== undefined) updatePayload.commission_rate = data.commission_rate; + + if (data.registration_date !== undefined) updatePayload.registration_date = data.registration_date; + + if (data.started_payment_date !== undefined) updatePayload.started_payment_date = data.started_payment_date; + + if (data.last_payment_date !== undefined) updatePayload.last_payment_date = data.last_payment_date; + + if (data.finish_payment_date !== undefined) updatePayload.finish_payment_date = data.finish_payment_date; + + if (data.finish_discount !== undefined) updatePayload.finish_discount = data.finish_discount; + + if (data.finish_discount_amount !== undefined) updatePayload.finish_discount_amount = data.finish_discount_amount; + + if (data.admin_rate !== undefined) updatePayload.admin_rate = data.admin_rate; + + if (data.admin_amount !== undefined) updatePayload.admin_amount = data.admin_amount; + + if (data.purpose !== undefined) updatePayload.purpose = data.purpose; + + updatePayload.updatedById = currentUser.id; + + await loans.update(updatePayload, {transaction}); + + return loans; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loans = await db.loans.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of loans) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of loans) { + await record.destroy({transaction}); + } + }); + + return loans; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const loans = await db.loans.findByPk(id, options); + + await loans.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await loans.destroy({ + transaction + }); + + return loans; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const loans = await db.loans.findOne( + { where }, + { transaction }, + ); + + if (!loans) { + return loans; + } + + const output = loans.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.code) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loans', + 'code', + filter.code, + ), + }; + } + + if (filter.purpose) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loans', + 'purpose', + filter.purpose, + ), + }; + } + + if (filter.principal_amountRange) { + const [start, end] = filter.principal_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + principal_amount: { + ...where.principal_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + principal_amount: { + ...where.principal_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.termRange) { + const [start, end] = filter.termRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + term: { + ...where.term, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + term: { + ...where.term, + [Op.lte]: end, + }, + }; + } + } + + if (filter.pending_amountRange) { + const [start, end] = filter.pending_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pending_amount: { + ...where.pending_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pending_amount: { + ...where.pending_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.last_pending_amountRange) { + const [start, end] = filter.last_pending_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + last_pending_amount: { + ...where.last_pending_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + last_pending_amount: { + ...where.last_pending_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.rateRange) { + const [start, end] = filter.rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + rate: { + ...where.rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + rate: { + ...where.rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.commission_rateRange) { + const [start, end] = filter.commission_rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + commission_rate: { + ...where.commission_rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + commission_rate: { + ...where.commission_rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.registration_dateRange) { + const [start, end] = filter.registration_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + registration_date: { + ...where.registration_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + registration_date: { + ...where.registration_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.started_payment_dateRange) { + const [start, end] = filter.started_payment_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + started_payment_date: { + ...where.started_payment_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + started_payment_date: { + ...where.started_payment_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.last_payment_dateRange) { + const [start, end] = filter.last_payment_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + last_payment_date: { + ...where.last_payment_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + last_payment_date: { + ...where.last_payment_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.finish_payment_dateRange) { + const [start, end] = filter.finish_payment_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + finish_payment_date: { + ...where.finish_payment_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + finish_payment_date: { + ...where.finish_payment_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.finish_discountRange) { + const [start, end] = filter.finish_discountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + finish_discount: { + ...where.finish_discount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + finish_discount: { + ...where.finish_discount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.finish_discount_amountRange) { + const [start, end] = filter.finish_discount_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + finish_discount_amount: { + ...where.finish_discount_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + finish_discount_amount: { + ...where.finish_discount_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.admin_rateRange) { + const [start, end] = filter.admin_rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + admin_rate: { + ...where.admin_rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + admin_rate: { + ...where.admin_rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.admin_amountRange) { + const [start, end] = filter.admin_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + admin_amount: { + ...where.admin_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + admin_amount: { + ...where.admin_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.loans.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'loans', + 'code', + query, + ), + ], + }; + } + + const records = await db.loans.findAll({ + attributes: [ 'id', 'code' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['code', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.code, + })); + } + +}; + diff --git a/backend/src/db/api/members.js b/backend/src/db/api/members.js new file mode 100644 index 0000000..3d1857b --- /dev/null +++ b/backend/src/db/api/members.js @@ -0,0 +1,313 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class MembersDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const members = await db.members.create( + { + id: data.id || undefined, + + name_kh: data.name_kh + || + null + , + + name_en: data.name_en + || + null + , + + phone_number: data.phone_number + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return members; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const membersData = data.map((item, index) => ({ + id: item.id || undefined, + + name_kh: item.name_kh + || + null + , + + name_en: item.name_en + || + null + , + + phone_number: item.phone_number + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const members = await db.members.bulkCreate(membersData, { transaction }); + + return members; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const members = await db.members.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + if (data.phone_number !== undefined) updatePayload.phone_number = data.phone_number; + + updatePayload.updatedById = currentUser.id; + + await members.update(updatePayload, {transaction}); + + return members; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const members = await db.members.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of members) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of members) { + await record.destroy({transaction}); + } + }); + + return members; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const members = await db.members.findByPk(id, options); + + await members.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await members.destroy({ + transaction + }); + + return members; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const members = await db.members.findOne( + { where }, + { transaction }, + ); + + if (!members) { + return members; + } + + const output = members.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'members', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'members', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.phone_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'members', + 'phone_number', + filter.phone_number, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.members.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'members', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.members.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/menus.js b/backend/src/db/api/menus.js new file mode 100644 index 0000000..f17d2d3 --- /dev/null +++ b/backend/src/db/api/menus.js @@ -0,0 +1,359 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class MenusDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const menus = await db.menus.create( + { + id: data.id || undefined, + + label: data.label + || + null + , + + url: data.url + || + null + , + + active_url: data.active_url + || + null + , + + permission: data.permission + || + null + , + + icon: data.icon + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return menus; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const menusData = data.map((item, index) => ({ + id: item.id || undefined, + + label: item.label + || + null + , + + url: item.url + || + null + , + + active_url: item.active_url + || + null + , + + permission: item.permission + || + null + , + + icon: item.icon + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const menus = await db.menus.bulkCreate(menusData, { transaction }); + + return menus; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const menus = await db.menus.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.label !== undefined) updatePayload.label = data.label; + + if (data.url !== undefined) updatePayload.url = data.url; + + if (data.active_url !== undefined) updatePayload.active_url = data.active_url; + + if (data.permission !== undefined) updatePayload.permission = data.permission; + + if (data.icon !== undefined) updatePayload.icon = data.icon; + + updatePayload.updatedById = currentUser.id; + + await menus.update(updatePayload, {transaction}); + + return menus; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const menus = await db.menus.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of menus) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of menus) { + await record.destroy({transaction}); + } + }); + + return menus; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const menus = await db.menus.findByPk(id, options); + + await menus.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await menus.destroy({ + transaction + }); + + return menus; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const menus = await db.menus.findOne( + { where }, + { transaction }, + ); + + if (!menus) { + return menus; + } + + const output = menus.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.label) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'menus', + 'label', + filter.label, + ), + }; + } + + if (filter.url) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'menus', + 'url', + filter.url, + ), + }; + } + + if (filter.active_url) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'menus', + 'active_url', + filter.active_url, + ), + }; + } + + if (filter.permission) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'menus', + 'permission', + filter.permission, + ), + }; + } + + if (filter.icon) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'menus', + 'icon', + filter.icon, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.menus.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'menus', + 'label', + query, + ), + ], + }; + } + + const records = await db.menus.findAll({ + attributes: [ 'id', 'label' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['label', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.label, + })); + } + +}; + diff --git a/backend/src/db/api/payment_revenues.js b/backend/src/db/api/payment_revenues.js new file mode 100644 index 0000000..8f27dab --- /dev/null +++ b/backend/src/db/api/payment_revenues.js @@ -0,0 +1,460 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Payment_revenuesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payment_revenues = await db.payment_revenues.create( + { + id: data.id || undefined, + + transaction_date: data.transaction_date + || + null + , + + admin_fee_amount: data.admin_fee_amount + || + null + , + + interest_amount: data.interest_amount + || + null + , + + commission_amount: data.commission_amount + || + null + , + + expense_amount: data.expense_amount + || + null + , + + setlement_datetime: data.setlement_datetime + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return payment_revenues; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const payment_revenuesData = data.map((item, index) => ({ + id: item.id || undefined, + + transaction_date: item.transaction_date + || + null + , + + admin_fee_amount: item.admin_fee_amount + || + null + , + + interest_amount: item.interest_amount + || + null + , + + commission_amount: item.commission_amount + || + null + , + + expense_amount: item.expense_amount + || + null + , + + setlement_datetime: item.setlement_datetime + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const payment_revenues = await db.payment_revenues.bulkCreate(payment_revenuesData, { transaction }); + + return payment_revenues; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payment_revenues = await db.payment_revenues.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.transaction_date !== undefined) updatePayload.transaction_date = data.transaction_date; + + if (data.admin_fee_amount !== undefined) updatePayload.admin_fee_amount = data.admin_fee_amount; + + if (data.interest_amount !== undefined) updatePayload.interest_amount = data.interest_amount; + + if (data.commission_amount !== undefined) updatePayload.commission_amount = data.commission_amount; + + if (data.expense_amount !== undefined) updatePayload.expense_amount = data.expense_amount; + + if (data.setlement_datetime !== undefined) updatePayload.setlement_datetime = data.setlement_datetime; + + updatePayload.updatedById = currentUser.id; + + await payment_revenues.update(updatePayload, {transaction}); + + return payment_revenues; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payment_revenues = await db.payment_revenues.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of payment_revenues) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of payment_revenues) { + await record.destroy({transaction}); + } + }); + + return payment_revenues; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payment_revenues = await db.payment_revenues.findByPk(id, options); + + await payment_revenues.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await payment_revenues.destroy({ + transaction + }); + + return payment_revenues; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const payment_revenues = await db.payment_revenues.findOne( + { where }, + { transaction }, + ); + + if (!payment_revenues) { + return payment_revenues; + } + + const output = payment_revenues.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.transaction_dateRange) { + const [start, end] = filter.transaction_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + transaction_date: { + ...where.transaction_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + transaction_date: { + ...where.transaction_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.admin_fee_amountRange) { + const [start, end] = filter.admin_fee_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + admin_fee_amount: { + ...where.admin_fee_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + admin_fee_amount: { + ...where.admin_fee_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.interest_amountRange) { + const [start, end] = filter.interest_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + interest_amount: { + ...where.interest_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + interest_amount: { + ...where.interest_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.commission_amountRange) { + const [start, end] = filter.commission_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + commission_amount: { + ...where.commission_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + commission_amount: { + ...where.commission_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.expense_amountRange) { + const [start, end] = filter.expense_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + expense_amount: { + ...where.expense_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + expense_amount: { + ...where.expense_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.setlement_datetimeRange) { + const [start, end] = filter.setlement_datetimeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + setlement_datetime: { + ...where.setlement_datetime, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + setlement_datetime: { + ...where.setlement_datetime, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.payment_revenues.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'payment_revenues', + 'transaction_date', + query, + ), + ], + }; + } + + const records = await db.payment_revenues.findAll({ + attributes: [ 'id', 'transaction_date' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['transaction_date', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.transaction_date, + })); + } + +}; + diff --git a/backend/src/db/api/payment_status.js b/backend/src/db/api/payment_status.js new file mode 100644 index 0000000..4efcf3e --- /dev/null +++ b/backend/src/db/api/payment_status.js @@ -0,0 +1,311 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Payment_statusDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payment_status = await db.payment_status.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + css: data.css + || + null + , + + visible: data.visible + || + false + + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return payment_status; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const payment_statusData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + css: item.css + || + null + , + + visible: item.visible + || + false + + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const payment_status = await db.payment_status.bulkCreate(payment_statusData, { transaction }); + + return payment_status; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payment_status = await db.payment_status.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.css !== undefined) updatePayload.css = data.css; + + if (data.visible !== undefined) updatePayload.visible = data.visible; + + updatePayload.updatedById = currentUser.id; + + await payment_status.update(updatePayload, {transaction}); + + return payment_status; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payment_status = await db.payment_status.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of payment_status) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of payment_status) { + await record.destroy({transaction}); + } + }); + + return payment_status; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payment_status = await db.payment_status.findByPk(id, options); + + await payment_status.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await payment_status.destroy({ + transaction + }); + + return payment_status; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const payment_status = await db.payment_status.findOne( + { where }, + { transaction }, + ); + + if (!payment_status) { + return payment_status; + } + + const output = payment_status.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'payment_status', + 'name', + filter.name, + ), + }; + } + + if (filter.css) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'payment_status', + 'css', + filter.css, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.visible) { + where = { + ...where, + visible: filter.visible, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.payment_status.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'payment_status', + 'name', + query, + ), + ], + }; + } + + const records = await db.payment_status.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/payment_transactions.js b/backend/src/db/api/payment_transactions.js new file mode 100644 index 0000000..f121596 --- /dev/null +++ b/backend/src/db/api/payment_transactions.js @@ -0,0 +1,515 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Payment_transactionsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payment_transactions = await db.payment_transactions.create( + { + id: data.id || undefined, + + transaction_datetime: data.transaction_datetime + || + null + , + + transaction_amount: data.transaction_amount + || + null + , + + deduct_amount: data.deduct_amount + || + null + , + + interest_amount: data.interest_amount + || + null + , + + commission_amount: data.commission_amount + || + null + , + + revenue_amount: data.revenue_amount + || + null + , + + setlement_datetime: data.setlement_datetime + || + null + , + + type: data.type + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return payment_transactions; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const payment_transactionsData = data.map((item, index) => ({ + id: item.id || undefined, + + transaction_datetime: item.transaction_datetime + || + null + , + + transaction_amount: item.transaction_amount + || + null + , + + deduct_amount: item.deduct_amount + || + null + , + + interest_amount: item.interest_amount + || + null + , + + commission_amount: item.commission_amount + || + null + , + + revenue_amount: item.revenue_amount + || + null + , + + setlement_datetime: item.setlement_datetime + || + null + , + + type: item.type + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const payment_transactions = await db.payment_transactions.bulkCreate(payment_transactionsData, { transaction }); + + return payment_transactions; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payment_transactions = await db.payment_transactions.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.transaction_datetime !== undefined) updatePayload.transaction_datetime = data.transaction_datetime; + + if (data.transaction_amount !== undefined) updatePayload.transaction_amount = data.transaction_amount; + + if (data.deduct_amount !== undefined) updatePayload.deduct_amount = data.deduct_amount; + + if (data.interest_amount !== undefined) updatePayload.interest_amount = data.interest_amount; + + if (data.commission_amount !== undefined) updatePayload.commission_amount = data.commission_amount; + + if (data.revenue_amount !== undefined) updatePayload.revenue_amount = data.revenue_amount; + + if (data.setlement_datetime !== undefined) updatePayload.setlement_datetime = data.setlement_datetime; + + if (data.type !== undefined) updatePayload.type = data.type; + + updatePayload.updatedById = currentUser.id; + + await payment_transactions.update(updatePayload, {transaction}); + + return payment_transactions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payment_transactions = await db.payment_transactions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of payment_transactions) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of payment_transactions) { + await record.destroy({transaction}); + } + }); + + return payment_transactions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payment_transactions = await db.payment_transactions.findByPk(id, options); + + await payment_transactions.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await payment_transactions.destroy({ + transaction + }); + + return payment_transactions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const payment_transactions = await db.payment_transactions.findOne( + { where }, + { transaction }, + ); + + if (!payment_transactions) { + return payment_transactions; + } + + const output = payment_transactions.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.transaction_datetimeRange) { + const [start, end] = filter.transaction_datetimeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + transaction_datetime: { + ...where.transaction_datetime, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + transaction_datetime: { + ...where.transaction_datetime, + [Op.lte]: end, + }, + }; + } + } + + if (filter.transaction_amountRange) { + const [start, end] = filter.transaction_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + transaction_amount: { + ...where.transaction_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + transaction_amount: { + ...where.transaction_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.deduct_amountRange) { + const [start, end] = filter.deduct_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + deduct_amount: { + ...where.deduct_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + deduct_amount: { + ...where.deduct_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.interest_amountRange) { + const [start, end] = filter.interest_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + interest_amount: { + ...where.interest_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + interest_amount: { + ...where.interest_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.commission_amountRange) { + const [start, end] = filter.commission_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + commission_amount: { + ...where.commission_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + commission_amount: { + ...where.commission_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.revenue_amountRange) { + const [start, end] = filter.revenue_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + revenue_amount: { + ...where.revenue_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + revenue_amount: { + ...where.revenue_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.setlement_datetimeRange) { + const [start, end] = filter.setlement_datetimeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + setlement_datetime: { + ...where.setlement_datetime, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + setlement_datetime: { + ...where.setlement_datetime, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.type) { + where = { + ...where, + type: filter.type, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.payment_transactions.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'payment_transactions', + 'transaction_amount', + query, + ), + ], + }; + } + + const records = await db.payment_transactions.findAll({ + attributes: [ 'id', 'transaction_amount' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['transaction_amount', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.transaction_amount, + })); + } + +}; + diff --git a/backend/src/db/api/payments.js b/backend/src/db/api/payments.js new file mode 100644 index 0000000..d6a13fe --- /dev/null +++ b/backend/src/db/api/payments.js @@ -0,0 +1,771 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class PaymentsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payments = await db.payments.create( + { + id: data.id || undefined, + + start_payment_date: data.start_payment_date + || + null + , + + payment_date: data.payment_date + || + null + , + + last_payment_paid_date: data.last_payment_paid_date + || + null + , + + sort: data.sort + || + null + , + + deduct_amount: data.deduct_amount + || + null + , + + deduct_paid_amount: data.deduct_paid_amount + || + null + , + + interval: data.interval + || + null + , + + interest_amount: data.interest_amount + || + null + , + + commission_amount: data.commission_amount + || + null + , + + total_amount: data.total_amount + || + null + , + + total_paid_amount: data.total_paid_amount + || + null + , + + penalty_amount: data.penalty_amount + || + null + , + + pending_amount: data.pending_amount + || + null + , + + cross_amount: data.cross_amount + || + null + , + + remark: data.remark + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return payments; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const paymentsData = data.map((item, index) => ({ + id: item.id || undefined, + + start_payment_date: item.start_payment_date + || + null + , + + payment_date: item.payment_date + || + null + , + + last_payment_paid_date: item.last_payment_paid_date + || + null + , + + sort: item.sort + || + null + , + + deduct_amount: item.deduct_amount + || + null + , + + deduct_paid_amount: item.deduct_paid_amount + || + null + , + + interval: item.interval + || + null + , + + interest_amount: item.interest_amount + || + null + , + + commission_amount: item.commission_amount + || + null + , + + total_amount: item.total_amount + || + null + , + + total_paid_amount: item.total_paid_amount + || + null + , + + penalty_amount: item.penalty_amount + || + null + , + + pending_amount: item.pending_amount + || + null + , + + cross_amount: item.cross_amount + || + null + , + + remark: item.remark + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const payments = await db.payments.bulkCreate(paymentsData, { transaction }); + + return payments; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payments = await db.payments.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.start_payment_date !== undefined) updatePayload.start_payment_date = data.start_payment_date; + + if (data.payment_date !== undefined) updatePayload.payment_date = data.payment_date; + + if (data.last_payment_paid_date !== undefined) updatePayload.last_payment_paid_date = data.last_payment_paid_date; + + if (data.sort !== undefined) updatePayload.sort = data.sort; + + if (data.deduct_amount !== undefined) updatePayload.deduct_amount = data.deduct_amount; + + if (data.deduct_paid_amount !== undefined) updatePayload.deduct_paid_amount = data.deduct_paid_amount; + + if (data.interval !== undefined) updatePayload.interval = data.interval; + + if (data.interest_amount !== undefined) updatePayload.interest_amount = data.interest_amount; + + if (data.commission_amount !== undefined) updatePayload.commission_amount = data.commission_amount; + + if (data.total_amount !== undefined) updatePayload.total_amount = data.total_amount; + + if (data.total_paid_amount !== undefined) updatePayload.total_paid_amount = data.total_paid_amount; + + if (data.penalty_amount !== undefined) updatePayload.penalty_amount = data.penalty_amount; + + if (data.pending_amount !== undefined) updatePayload.pending_amount = data.pending_amount; + + if (data.cross_amount !== undefined) updatePayload.cross_amount = data.cross_amount; + + if (data.remark !== undefined) updatePayload.remark = data.remark; + + updatePayload.updatedById = currentUser.id; + + await payments.update(updatePayload, {transaction}); + + return payments; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const payments = await db.payments.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of payments) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of payments) { + await record.destroy({transaction}); + } + }); + + return payments; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const payments = await db.payments.findByPk(id, options); + + await payments.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await payments.destroy({ + transaction + }); + + return payments; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const payments = await db.payments.findOne( + { where }, + { transaction }, + ); + + if (!payments) { + return payments; + } + + const output = payments.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.remark) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'payments', + 'remark', + filter.remark, + ), + }; + } + + if (filter.start_payment_dateRange) { + const [start, end] = filter.start_payment_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + start_payment_date: { + ...where.start_payment_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + start_payment_date: { + ...where.start_payment_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.payment_dateRange) { + const [start, end] = filter.payment_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + payment_date: { + ...where.payment_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + payment_date: { + ...where.payment_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.last_payment_paid_dateRange) { + const [start, end] = filter.last_payment_paid_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + last_payment_paid_date: { + ...where.last_payment_paid_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + last_payment_paid_date: { + ...where.last_payment_paid_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.sortRange) { + const [start, end] = filter.sortRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + sort: { + ...where.sort, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + sort: { + ...where.sort, + [Op.lte]: end, + }, + }; + } + } + + if (filter.deduct_amountRange) { + const [start, end] = filter.deduct_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + deduct_amount: { + ...where.deduct_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + deduct_amount: { + ...where.deduct_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.deduct_paid_amountRange) { + const [start, end] = filter.deduct_paid_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + deduct_paid_amount: { + ...where.deduct_paid_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + deduct_paid_amount: { + ...where.deduct_paid_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.intervalRange) { + const [start, end] = filter.intervalRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + interval: { + ...where.interval, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + interval: { + ...where.interval, + [Op.lte]: end, + }, + }; + } + } + + if (filter.interest_amountRange) { + const [start, end] = filter.interest_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + interest_amount: { + ...where.interest_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + interest_amount: { + ...where.interest_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.commission_amountRange) { + const [start, end] = filter.commission_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + commission_amount: { + ...where.commission_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + commission_amount: { + ...where.commission_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.total_amountRange) { + const [start, end] = filter.total_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + total_amount: { + ...where.total_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + total_amount: { + ...where.total_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.total_paid_amountRange) { + const [start, end] = filter.total_paid_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + total_paid_amount: { + ...where.total_paid_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + total_paid_amount: { + ...where.total_paid_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.penalty_amountRange) { + const [start, end] = filter.penalty_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + penalty_amount: { + ...where.penalty_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + penalty_amount: { + ...where.penalty_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.pending_amountRange) { + const [start, end] = filter.pending_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pending_amount: { + ...where.pending_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pending_amount: { + ...where.pending_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.cross_amountRange) { + const [start, end] = filter.cross_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + cross_amount: { + ...where.cross_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + cross_amount: { + ...where.cross_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.payments.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'payments', + 'remark', + query, + ), + ], + }; + } + + const records = await db.payments.findAll({ + attributes: [ 'id', 'remark' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['remark', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.remark, + })); + } + +}; + diff --git a/backend/src/db/api/provinces.js b/backend/src/db/api/provinces.js new file mode 100644 index 0000000..d9150dc --- /dev/null +++ b/backend/src/db/api/provinces.js @@ -0,0 +1,311 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class ProvincesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const provinces = await db.provinces.create( + { + id: data.id || undefined, + + name_kh: data.name_kh + || + null + , + + name_en: data.name_en + || + null + , + + active: data.active + || + false + + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return provinces; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const provincesData = data.map((item, index) => ({ + id: item.id || undefined, + + name_kh: item.name_kh + || + null + , + + name_en: item.name_en + || + null + , + + active: item.active + || + false + + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const provinces = await db.provinces.bulkCreate(provincesData, { transaction }); + + return provinces; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const provinces = await db.provinces.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + if (data.active !== undefined) updatePayload.active = data.active; + + updatePayload.updatedById = currentUser.id; + + await provinces.update(updatePayload, {transaction}); + + return provinces; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const provinces = await db.provinces.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of provinces) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of provinces) { + await record.destroy({transaction}); + } + }); + + return provinces; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const provinces = await db.provinces.findByPk(id, options); + + await provinces.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await provinces.destroy({ + transaction + }); + + return provinces; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const provinces = await db.provinces.findOne( + { where }, + { transaction }, + ); + + if (!provinces) { + return provinces; + } + + const output = provinces.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'provinces', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'provinces', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.active) { + where = { + ...where, + active: filter.active, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.provinces.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'provinces', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.provinces.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/sexes.js b/backend/src/db/api/sexes.js new file mode 100644 index 0000000..045ae92 --- /dev/null +++ b/backend/src/db/api/sexes.js @@ -0,0 +1,267 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class SexesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const sexes = await db.sexes.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return sexes; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const sexesData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const sexes = await db.sexes.bulkCreate(sexesData, { transaction }); + + return sexes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const sexes = await db.sexes.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await sexes.update(updatePayload, {transaction}); + + return sexes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const sexes = await db.sexes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of sexes) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of sexes) { + await record.destroy({transaction}); + } + }); + + return sexes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const sexes = await db.sexes.findByPk(id, options); + + await sexes.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await sexes.destroy({ + transaction + }); + + return sexes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const sexes = await db.sexes.findOne( + { where }, + { transaction }, + ); + + if (!sexes) { + return sexes; + } + + const output = sexes.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'sexes', + 'name', + filter.name, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.sexes.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'sexes', + 'name', + query, + ), + ], + }; + } + + const records = await db.sexes.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/shareholders.js b/backend/src/db/api/shareholders.js new file mode 100644 index 0000000..74daef1 --- /dev/null +++ b/backend/src/db/api/shareholders.js @@ -0,0 +1,555 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class ShareholdersDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const shareholders = await db.shareholders.create( + { + id: data.id || undefined, + + name_en: data.name_en + || + null + , + + name_kh: data.name_kh + || + null + , + + earn_rate: data.earn_rate + || + null + , + + date_of_birth: data.date_of_birth + || + null + , + + phone_number: data.phone_number + || + null + , + + start_work_date: data.start_work_date + || + null + , + + born_place: data.born_place + || + null + , + + document_type: data.document_type + || + null + , + + document_number: data.document_number + || + null + , + + emergency_number: data.emergency_number + || + null + , + + current_place: data.current_place + || + null + , + + sex: data.sex + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return shareholders; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const shareholdersData = data.map((item, index) => ({ + id: item.id || undefined, + + name_en: item.name_en + || + null + , + + name_kh: item.name_kh + || + null + , + + earn_rate: item.earn_rate + || + null + , + + date_of_birth: item.date_of_birth + || + null + , + + phone_number: item.phone_number + || + null + , + + start_work_date: item.start_work_date + || + null + , + + born_place: item.born_place + || + null + , + + document_type: item.document_type + || + null + , + + document_number: item.document_number + || + null + , + + emergency_number: item.emergency_number + || + null + , + + current_place: item.current_place + || + null + , + + sex: item.sex + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const shareholders = await db.shareholders.bulkCreate(shareholdersData, { transaction }); + + return shareholders; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const shareholders = await db.shareholders.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.earn_rate !== undefined) updatePayload.earn_rate = data.earn_rate; + + if (data.date_of_birth !== undefined) updatePayload.date_of_birth = data.date_of_birth; + + if (data.phone_number !== undefined) updatePayload.phone_number = data.phone_number; + + if (data.start_work_date !== undefined) updatePayload.start_work_date = data.start_work_date; + + if (data.born_place !== undefined) updatePayload.born_place = data.born_place; + + if (data.document_type !== undefined) updatePayload.document_type = data.document_type; + + if (data.document_number !== undefined) updatePayload.document_number = data.document_number; + + if (data.emergency_number !== undefined) updatePayload.emergency_number = data.emergency_number; + + if (data.current_place !== undefined) updatePayload.current_place = data.current_place; + + if (data.sex !== undefined) updatePayload.sex = data.sex; + + updatePayload.updatedById = currentUser.id; + + await shareholders.update(updatePayload, {transaction}); + + return shareholders; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const shareholders = await db.shareholders.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of shareholders) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of shareholders) { + await record.destroy({transaction}); + } + }); + + return shareholders; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const shareholders = await db.shareholders.findByPk(id, options); + + await shareholders.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await shareholders.destroy({ + transaction + }); + + return shareholders; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const shareholders = await db.shareholders.findOne( + { where }, + { transaction }, + ); + + if (!shareholders) { + return shareholders; + } + + const output = shareholders.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.phone_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'phone_number', + filter.phone_number, + ), + }; + } + + if (filter.born_place) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'born_place', + filter.born_place, + ), + }; + } + + if (filter.document_type) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'document_type', + filter.document_type, + ), + }; + } + + if (filter.document_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'document_number', + filter.document_number, + ), + }; + } + + if (filter.emergency_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'emergency_number', + filter.emergency_number, + ), + }; + } + + if (filter.current_place) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'shareholders', + 'current_place', + filter.current_place, + ), + }; + } + + if (filter.earn_rateRange) { + const [start, end] = filter.earn_rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + earn_rate: { + ...where.earn_rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + earn_rate: { + ...where.earn_rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.date_of_birthRange) { + const [start, end] = filter.date_of_birthRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.lte]: end, + }, + }; + } + } + + if (filter.start_work_dateRange) { + const [start, end] = filter.start_work_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + start_work_date: { + ...where.start_work_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + start_work_date: { + ...where.start_work_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.sex) { + where = { + ...where, + sex: filter.sex, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.shareholders.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'shareholders', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.shareholders.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/staff_status.js b/backend/src/db/api/staff_status.js new file mode 100644 index 0000000..1c9022f --- /dev/null +++ b/backend/src/db/api/staff_status.js @@ -0,0 +1,290 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Staff_statusDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const staff_status = await db.staff_status.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + css: data.css + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return staff_status; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const staff_statusData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + css: item.css + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const staff_status = await db.staff_status.bulkCreate(staff_statusData, { transaction }); + + return staff_status; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const staff_status = await db.staff_status.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.css !== undefined) updatePayload.css = data.css; + + updatePayload.updatedById = currentUser.id; + + await staff_status.update(updatePayload, {transaction}); + + return staff_status; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const staff_status = await db.staff_status.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of staff_status) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of staff_status) { + await record.destroy({transaction}); + } + }); + + return staff_status; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const staff_status = await db.staff_status.findByPk(id, options); + + await staff_status.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await staff_status.destroy({ + transaction + }); + + return staff_status; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const staff_status = await db.staff_status.findOne( + { where }, + { transaction }, + ); + + if (!staff_status) { + return staff_status; + } + + const output = staff_status.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staff_status', + 'name', + filter.name, + ), + }; + } + + if (filter.css) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staff_status', + 'css', + filter.css, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.staff_status.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'staff_status', + 'name', + query, + ), + ], + }; + } + + const records = await db.staff_status.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/staffs.js b/backend/src/db/api/staffs.js new file mode 100644 index 0000000..565387d --- /dev/null +++ b/backend/src/db/api/staffs.js @@ -0,0 +1,519 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class StaffsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const staffs = await db.staffs.create( + { + id: data.id || undefined, + + name_en: data.name_en + || + null + , + + name_kh: data.name_kh + || + null + , + + date_of_birth: data.date_of_birth + || + null + , + + phone_number: data.phone_number + || + null + , + + start_work_date: data.start_work_date + || + null + , + + born_place: data.born_place + || + null + , + + document_type: data.document_type + || + null + , + + document_number: data.document_number + || + null + , + + emergency_number: data.emergency_number + || + null + , + + current_place: data.current_place + || + null + , + + sex: data.sex + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return staffs; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const staffsData = data.map((item, index) => ({ + id: item.id || undefined, + + name_en: item.name_en + || + null + , + + name_kh: item.name_kh + || + null + , + + date_of_birth: item.date_of_birth + || + null + , + + phone_number: item.phone_number + || + null + , + + start_work_date: item.start_work_date + || + null + , + + born_place: item.born_place + || + null + , + + document_type: item.document_type + || + null + , + + document_number: item.document_number + || + null + , + + emergency_number: item.emergency_number + || + null + , + + current_place: item.current_place + || + null + , + + sex: item.sex + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const staffs = await db.staffs.bulkCreate(staffsData, { transaction }); + + return staffs; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const staffs = await db.staffs.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.date_of_birth !== undefined) updatePayload.date_of_birth = data.date_of_birth; + + if (data.phone_number !== undefined) updatePayload.phone_number = data.phone_number; + + if (data.start_work_date !== undefined) updatePayload.start_work_date = data.start_work_date; + + if (data.born_place !== undefined) updatePayload.born_place = data.born_place; + + if (data.document_type !== undefined) updatePayload.document_type = data.document_type; + + if (data.document_number !== undefined) updatePayload.document_number = data.document_number; + + if (data.emergency_number !== undefined) updatePayload.emergency_number = data.emergency_number; + + if (data.current_place !== undefined) updatePayload.current_place = data.current_place; + + if (data.sex !== undefined) updatePayload.sex = data.sex; + + updatePayload.updatedById = currentUser.id; + + await staffs.update(updatePayload, {transaction}); + + return staffs; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const staffs = await db.staffs.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of staffs) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of staffs) { + await record.destroy({transaction}); + } + }); + + return staffs; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const staffs = await db.staffs.findByPk(id, options); + + await staffs.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await staffs.destroy({ + transaction + }); + + return staffs; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const staffs = await db.staffs.findOne( + { where }, + { transaction }, + ); + + if (!staffs) { + return staffs; + } + + const output = staffs.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.phone_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'phone_number', + filter.phone_number, + ), + }; + } + + if (filter.born_place) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'born_place', + filter.born_place, + ), + }; + } + + if (filter.document_type) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'document_type', + filter.document_type, + ), + }; + } + + if (filter.document_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'document_number', + filter.document_number, + ), + }; + } + + if (filter.emergency_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'emergency_number', + filter.emergency_number, + ), + }; + } + + if (filter.current_place) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'staffs', + 'current_place', + filter.current_place, + ), + }; + } + + if (filter.date_of_birthRange) { + const [start, end] = filter.date_of_birthRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + date_of_birth: { + ...where.date_of_birth, + [Op.lte]: end, + }, + }; + } + } + + if (filter.start_work_dateRange) { + const [start, end] = filter.start_work_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + start_work_date: { + ...where.start_work_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + start_work_date: { + ...where.start_work_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.sex) { + where = { + ...where, + sex: filter.sex, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.staffs.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'staffs', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.staffs.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/api/urls.js b/backend/src/db/api/urls.js new file mode 100644 index 0000000..61088ed --- /dev/null +++ b/backend/src/db/api/urls.js @@ -0,0 +1,351 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class UrlsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const urls = await db.urls.create( + { + id: data.id || undefined, + + method: data.method + || + null + , + + uri: data.uri + || + null + , + + route_name: data.route_name + || + null + , + + acitve: data.acitve + || + false + + , + + is_menu: data.is_menu + || + false + + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return urls; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const urlsData = data.map((item, index) => ({ + id: item.id || undefined, + + method: item.method + || + null + , + + uri: item.uri + || + null + , + + route_name: item.route_name + || + null + , + + acitve: item.acitve + || + false + + , + + is_menu: item.is_menu + || + false + + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const urls = await db.urls.bulkCreate(urlsData, { transaction }); + + return urls; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const urls = await db.urls.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.method !== undefined) updatePayload.method = data.method; + + if (data.uri !== undefined) updatePayload.uri = data.uri; + + if (data.route_name !== undefined) updatePayload.route_name = data.route_name; + + if (data.acitve !== undefined) updatePayload.acitve = data.acitve; + + if (data.is_menu !== undefined) updatePayload.is_menu = data.is_menu; + + updatePayload.updatedById = currentUser.id; + + await urls.update(updatePayload, {transaction}); + + return urls; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const urls = await db.urls.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of urls) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of urls) { + await record.destroy({transaction}); + } + }); + + return urls; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const urls = await db.urls.findByPk(id, options); + + await urls.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await urls.destroy({ + transaction + }); + + return urls; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const urls = await db.urls.findOne( + { where }, + { transaction }, + ); + + if (!urls) { + return urls; + } + + const output = urls.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.uri) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'urls', + 'uri', + filter.uri, + ), + }; + } + + if (filter.route_name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'urls', + 'route_name', + filter.route_name, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.method) { + where = { + ...where, + method: filter.method, + }; + } + + if (filter.acitve) { + where = { + ...where, + acitve: filter.acitve, + }; + } + + if (filter.is_menu) { + where = { + ...where, + is_menu: filter.is_menu, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.urls.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'urls', + 'uri', + query, + ), + ], + }; + } + + const records = await db.urls.findAll({ + attributes: [ 'id', 'uri' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['uri', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.uri, + })); + } + +}; + diff --git a/backend/src/db/api/user_has_menu.js b/backend/src/db/api/user_has_menu.js new file mode 100644 index 0000000..557d33c --- /dev/null +++ b/backend/src/db/api/user_has_menu.js @@ -0,0 +1,267 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class User_has_menuDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const user_has_menu = await db.user_has_menu.create( + { + id: data.id || undefined, + + status: data.status + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return user_has_menu; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const user_has_menuData = data.map((item, index) => ({ + id: item.id || undefined, + + status: item.status + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const user_has_menu = await db.user_has_menu.bulkCreate(user_has_menuData, { transaction }); + + return user_has_menu; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const user_has_menu = await db.user_has_menu.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await user_has_menu.update(updatePayload, {transaction}); + + return user_has_menu; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const user_has_menu = await db.user_has_menu.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of user_has_menu) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of user_has_menu) { + await record.destroy({transaction}); + } + }); + + return user_has_menu; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const user_has_menu = await db.user_has_menu.findByPk(id, options); + + await user_has_menu.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await user_has_menu.destroy({ + transaction + }); + + return user_has_menu; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const user_has_menu = await db.user_has_menu.findOne( + { where }, + { transaction }, + ); + + if (!user_has_menu) { + return user_has_menu; + } + + const output = user_has_menu.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.status) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'user_has_menu', + 'status', + filter.status, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.user_has_menu.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'user_has_menu', + 'status', + query, + ), + ], + }; + } + + const records = await db.user_has_menu.findAll({ + attributes: [ 'id', 'status' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['status', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.status, + })); + } + +}; + diff --git a/backend/src/db/api/user_type_urls.js b/backend/src/db/api/user_type_urls.js new file mode 100644 index 0000000..67f818b --- /dev/null +++ b/backend/src/db/api/user_type_urls.js @@ -0,0 +1,244 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class User_type_urlsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const user_type_urls = await db.user_type_urls.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return user_type_urls; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const user_type_urlsData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const user_type_urls = await db.user_type_urls.bulkCreate(user_type_urlsData, { transaction }); + + return user_type_urls; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const user_type_urls = await db.user_type_urls.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await user_type_urls.update(updatePayload, {transaction}); + + return user_type_urls; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const user_type_urls = await db.user_type_urls.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of user_type_urls) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of user_type_urls) { + await record.destroy({transaction}); + } + }); + + return user_type_urls; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const user_type_urls = await db.user_type_urls.findByPk(id, options); + + await user_type_urls.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await user_type_urls.destroy({ + transaction + }); + + return user_type_urls; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const user_type_urls = await db.user_type_urls.findOne( + { where }, + { transaction }, + ); + + if (!user_type_urls) { + return user_type_urls; + } + + const output = user_type_urls.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.user_type_urls.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'user_type_urls', + 'user_type', + query, + ), + ], + }; + } + + const records = await db.user_type_urls.findAll({ + attributes: [ 'id', 'user_type' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['user_type', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.user_type, + })); + } + +}; + diff --git a/backend/src/db/api/user_types.js b/backend/src/db/api/user_types.js new file mode 100644 index 0000000..b117f4e --- /dev/null +++ b/backend/src/db/api/user_types.js @@ -0,0 +1,288 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class User_typesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const user_types = await db.user_types.create( + { + id: data.id || undefined, + + is_admin: data.is_admin + || + false + + , + + name: data.name + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return user_types; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const user_typesData = data.map((item, index) => ({ + id: item.id || undefined, + + is_admin: item.is_admin + || + false + + , + + name: item.name + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const user_types = await db.user_types.bulkCreate(user_typesData, { transaction }); + + return user_types; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const user_types = await db.user_types.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.is_admin !== undefined) updatePayload.is_admin = data.is_admin; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await user_types.update(updatePayload, {transaction}); + + return user_types; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const user_types = await db.user_types.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of user_types) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of user_types) { + await record.destroy({transaction}); + } + }); + + return user_types; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const user_types = await db.user_types.findByPk(id, options); + + await user_types.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await user_types.destroy({ + transaction + }); + + return user_types; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const user_types = await db.user_types.findOne( + { where }, + { transaction }, + ); + + if (!user_types) { + return user_types; + } + + const output = user_types.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'user_types', + 'name', + filter.name, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.is_admin) { + where = { + ...where, + is_admin: filter.is_admin, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.user_types.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'user_types', + 'name', + query, + ), + ], + }; + } + + const records = await db.user_types.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js new file mode 100644 index 0000000..8b4828e --- /dev/null +++ b/backend/src/db/api/users.js @@ -0,0 +1,692 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const bcrypt = require('bcrypt'); +const config = require('../../config'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class UsersDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const users = await db.users.create( + { + id: data.data.id || undefined, + + firstName: data.data.firstName + || + null, + + lastName: data.data.lastName + || + null, + + phoneNumber: data.data.phoneNumber + || + null, + + email: data.data.email + || + null, + + disabled: data.data.disabled + || + false + , + + password: data.data.password + || + null, + + emailVerified: data.data.emailVerified + || + true + , + + emailVerificationToken: data.data.emailVerificationToken + || + null, + + emailVerificationTokenExpiresAt: data.data.emailVerificationTokenExpiresAt + || + null, + + passwordResetToken: data.data.passwordResetToken + || + null, + + passwordResetTokenExpiresAt: data.data.passwordResetTokenExpiresAt + || + null, + + provider: data.data.provider + || + null, + + importHash: data.data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return users; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const usersData = data.map((item, index) => ({ + id: item.id || undefined, + + firstName: item.firstName + || + null + , + + lastName: item.lastName + || + null + , + + phoneNumber: item.phoneNumber + || + null + , + + email: item.email + || + null + , + + disabled: item.disabled + || + false + + , + + password: item.password + || + null + , + + emailVerified: item.emailVerified + || + false + + , + + emailVerificationToken: item.emailVerificationToken + || + null + , + + emailVerificationTokenExpiresAt: item.emailVerificationTokenExpiresAt + || + null + , + + passwordResetToken: item.passwordResetToken + || + null + , + + passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt + || + null + , + + provider: item.provider + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const users = await db.users.bulkCreate(usersData, { transaction }); + + return users; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, {}, {transaction}); + + if (!data?.app_role) { + data.app_role = users?.app_role?.id; + } + if (!data?.custom_permissions) { + data.custom_permissions = users?.custom_permissions?.map(item => item.id); + } + + if (data.password) { + data.password = bcrypt.hashSync( + data.password, + config.bcrypt.saltRounds, + ); + } else { + data.password = users.password; + } + + const updatePayload = {}; + + if (data.firstName !== undefined) updatePayload.firstName = data.firstName; + + if (data.lastName !== undefined) updatePayload.lastName = data.lastName; + + if (data.phoneNumber !== undefined) updatePayload.phoneNumber = data.phoneNumber; + + if (data.email !== undefined) updatePayload.email = data.email; + + if (data.disabled !== undefined) updatePayload.disabled = data.disabled; + + if (data.password !== undefined) updatePayload.password = data.password; + + if (data.emailVerified !== undefined) updatePayload.emailVerified = data.emailVerified; + + else updatePayload.emailVerified = true; + + if (data.emailVerificationToken !== undefined) updatePayload.emailVerificationToken = data.emailVerificationToken; + + if (data.emailVerificationTokenExpiresAt !== undefined) updatePayload.emailVerificationTokenExpiresAt = data.emailVerificationTokenExpiresAt; + + if (data.passwordResetToken !== undefined) updatePayload.passwordResetToken = data.passwordResetToken; + + if (data.passwordResetTokenExpiresAt !== undefined) updatePayload.passwordResetTokenExpiresAt = data.passwordResetTokenExpiresAt; + + if (data.provider !== undefined) updatePayload.provider = data.provider; + + updatePayload.updatedById = currentUser.id; + + await users.update(updatePayload, {transaction}); + + return users; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of users) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of users) { + await record.destroy({transaction}); + } + }); + + return users; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, options); + + await users.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await users.destroy({ + transaction + }); + + return users; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findOne( + { where }, + { transaction }, + ); + + if (!users) { + return users; + } + + const output = users.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.firstName) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'firstName', + filter.firstName, + ), + }; + } + + if (filter.lastName) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'lastName', + filter.lastName, + ), + }; + } + + if (filter.phoneNumber) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'phoneNumber', + filter.phoneNumber, + ), + }; + } + + if (filter.email) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'email', + filter.email, + ), + }; + } + + if (filter.password) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'password', + filter.password, + ), + }; + } + + if (filter.emailVerificationToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'emailVerificationToken', + filter.emailVerificationToken, + ), + }; + } + + if (filter.passwordResetToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'passwordResetToken', + filter.passwordResetToken, + ), + }; + } + + if (filter.provider) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'provider', + filter.provider, + ), + }; + } + + if (filter.emailVerificationTokenExpiresAtRange) { + const [start, end] = filter.emailVerificationTokenExpiresAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + + if (filter.passwordResetTokenExpiresAtRange) { + const [start, end] = filter.passwordResetTokenExpiresAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.disabled) { + where = { + ...where, + disabled: filter.disabled, + }; + } + + if (filter.emailVerified) { + where = { + ...where, + emailVerified: filter.emailVerified, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.users.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'users', + 'firstName', + query, + ), + ], + }; + } + + const records = await db.users.findAll({ + attributes: [ 'id', 'firstName' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['firstName', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.firstName, + })); + } + + static async createFromAuth(data, options) { + const transaction = (options && options.transaction) || undefined; + const users = await db.users.create( + { + email: data.email, + firstName: data.firstName, + authenticationUid: data.authenticationUid, + password: data.password, + }, + { transaction }, + ); + + const app_role = await db.roles.findOne({ + where: { name: config.roles?.user || "User" }, + }); + if (app_role?.id) { + await users.setApp_role(app_role?.id || null, { + transaction, + }); + } + + await users.update( + { + authenticationUid: users.id, + }, + { transaction }, + ); + + delete users.password; + return users; + } + + static async updatePassword(id, password, options) { + const currentUser = (options && options.currentUser) || { id: null }; + + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + password, + authenticationUid: id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return users; + } + + static async generateEmailVerificationToken(email, options) { + return this._generateToken(['emailVerificationToken', 'emailVerificationTokenExpiresAt'], email, options); + } + + static async generatePasswordResetToken(email, options) { + return this._generateToken(['passwordResetToken', 'passwordResetTokenExpiresAt'], email, options); + } + + static async findByPasswordResetToken(token, options) { + const transaction = (options && options.transaction) || undefined; + + return db.users.findOne( + { + where: { + passwordResetToken: token, + passwordResetTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + }, + { transaction }, + ); + } + + static async findByEmailVerificationToken(token, options) { + const transaction = (options && options.transaction) || undefined; + return db.users.findOne( + { + where: { + emailVerificationToken: token, + emailVerificationTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + }, + { transaction }, + ); + } + + static async markEmailVerified(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + emailVerified: true, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return true; + } + + static async _generateToken(keyNames, email, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + const users = await db.users.findOne( + { + where: { email: email.toLowerCase() }, + }, + { + transaction, + }, + ); + + const token = crypto + .randomBytes(20) + .toString('hex'); + const tokenExpiresAt = Date.now() + 360000; + + if(users){ + await users.update( + { + [keyNames[0]]: token, + [keyNames[1]]: tokenExpiresAt, + updatedById: currentUser.id, + }, + {transaction}, + ); + } + + return token; + } + +}; + diff --git a/backend/src/db/api/villages.js b/backend/src/db/api/villages.js new file mode 100644 index 0000000..8a9baf1 --- /dev/null +++ b/backend/src/db/api/villages.js @@ -0,0 +1,290 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class VillagesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const villages = await db.villages.create( + { + id: data.id || undefined, + + name_kh: data.name_kh + || + null + , + + name_en: data.name_en + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return villages; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const villagesData = data.map((item, index) => ({ + id: item.id || undefined, + + name_kh: item.name_kh + || + null + , + + name_en: item.name_en + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const villages = await db.villages.bulkCreate(villagesData, { transaction }); + + return villages; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const villages = await db.villages.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.name_kh !== undefined) updatePayload.name_kh = data.name_kh; + + if (data.name_en !== undefined) updatePayload.name_en = data.name_en; + + updatePayload.updatedById = currentUser.id; + + await villages.update(updatePayload, {transaction}); + + return villages; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const villages = await db.villages.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of villages) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of villages) { + await record.destroy({transaction}); + } + }); + + return villages; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const villages = await db.villages.findByPk(id, options); + + await villages.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await villages.destroy({ + transaction + }); + + return villages; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const villages = await db.villages.findOne( + { where }, + { transaction }, + ); + + if (!villages) { + return villages; + } + + const output = villages.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name_kh) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'villages', + 'name_kh', + filter.name_kh, + ), + }; + } + + if (filter.name_en) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'villages', + 'name_en', + filter.name_en, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.villages.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'villages', + 'name_en', + query, + ), + ], + }; + } + + const records = await db.villages.findAll({ + attributes: [ 'id', 'name_en' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name_en', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name_en, + })); + } + +}; + diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js new file mode 100644 index 0000000..928147e --- /dev/null +++ b/backend/src/db/db.config.js @@ -0,0 +1,33 @@ + + +module.exports = { + production: { + dialect: 'postgres', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + logging: console.log, + seederStorage: 'sequelize', + }, + development: { + username: 'postgres', + dialect: 'postgres', + password: '', + database: 'db_meng_leap_cash', + host: process.env.DB_HOST || 'localhost', + logging: console.log, + seederStorage: 'sequelize', + }, + dev_stage: { + dialect: 'postgres', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + logging: console.log, + seederStorage: 'sequelize', + } +}; diff --git a/backend/src/db/migrations/1751985526233.js b/backend/src/db/migrations/1751985526233.js new file mode 100644 index 0000000..5b9e08b --- /dev/null +++ b/backend/src/db/migrations/1751985526233.js @@ -0,0 +1,4470 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.createTable('users', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('branches', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('calendars', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('client_status', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('clients', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('communes', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('deposits', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('districts', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('document_type', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('expense_items', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('expense_types', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('group_menus', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('guarantor', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('interest_rates', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('loan_status', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('loan_types', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('loans', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('members', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('menus', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('payment_revenues', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('payment_status', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('payment_transactions', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('payments', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('provinces', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('sexes', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('shareholders', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('staff_status', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('staffs', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('urls', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('user_has_menu', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('user_type_urls', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('user_types', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.createTable('villages', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await queryInterface.addColumn( + 'users', + 'firstName', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'lastName', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'phoneNumber', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'email', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'disabled', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'password', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'emailVerified', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'emailVerificationToken', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'emailVerificationTokenExpiresAt', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'passwordResetToken', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'passwordResetTokenExpiresAt', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'users', + 'provider', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'branches', + 'code', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'branches', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'branches', + 'description', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'calendars', + 'date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'calendars', + 'is_weekend', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'calendars', + 'is_holiday', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'calendars', + 'description', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'calendars', + 'flag', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'client_status', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'code', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'date_of_birth', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'phone_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'is_new', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'document_typeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'document_type', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'document_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'sex', + { + type: Sequelize.DataTypes.ENUM, + + values: ['M','F'], + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'statusId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'client_status', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'userId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'provinceId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'provinces', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'districtId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'districts', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'communeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'communes', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'villageId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'villages', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'clients', + 'branchId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'branches', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'communes', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'communes', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'communes', + 'districtId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'districts', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'deposits', + 'deposit_datetime', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'deposits', + 'deposit_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'deposits', + 'shareholderId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'shareholders', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'deposits', + 'branchId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'branches', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'districts', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'districts', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'districts', + 'provinceId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'provinces', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'document_type', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'expense_items', + 'expense_datetime', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'expense_items', + 'description', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'expense_items', + 'expense_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'expense_items', + 'expense_typeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'expense_types', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'expense_items', + 'branchId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'branches', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'expense_types', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'expense_types', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'group_menus', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'group_menus', + 'is_admin', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'full_name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'sex', + { + type: Sequelize.DataTypes.ENUM, + + values: ['M','F'], + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'date_of_birth', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'document_type', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'document_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'phone_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'provinceId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'provinces', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'districtId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'districts', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'communeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'communes', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'villageId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'villages', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'guarantor', + 'full_address_input', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'code', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'rate', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'commission_rate', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'interval', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'sort', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'css', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'interest_rates', + 'setting', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loan_status', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loan_status', + 'css', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loan_types', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loan_types', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'code', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'principal_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'term', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'pending_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'last_pending_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'rate', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'commission_rate', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'registration_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'started_payment_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'last_payment_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'finish_payment_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'finish_discount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'finish_discount_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'admin_rate', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'admin_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'purpose', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'statusId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'loan_status', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'clientId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'clients', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'staffId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'staffs', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'interest_rateId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'interest_rates', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'branchId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'branches', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'loans', + 'loan_typeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'loan_types', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'members', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'members', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'members', + 'phone_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'members', + 'sexId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'sexes', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'parentId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'menus', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'label', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'url', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'active_url', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'permission', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'icon', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'urlId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'urls', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'menus', + 'groupId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'group_menus', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_revenues', + 'transaction_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_revenues', + 'admin_fee_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_revenues', + 'interest_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_revenues', + 'commission_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_revenues', + 'expense_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_revenues', + 'setlement_datetime', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_revenues', + 'branchId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'branches', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_status', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_status', + 'css', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_status', + 'visible', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'transaction_datetime', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'transaction_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'deduct_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'interest_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'commission_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'revenue_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'setlement_datetime', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'type', + { + type: Sequelize.DataTypes.ENUM, + + values: ['interest','deduction','reverse'], + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payment_transactions', + 'paymentId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'payments', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'start_payment_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'payment_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'last_payment_paid_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'sort', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'deduct_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'deduct_paid_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'interval', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'interest_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'commission_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'total_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'total_paid_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'penalty_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'pending_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'cross_amount', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'remark', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'loanId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'loans', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'payments', + 'statusId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'payment_status', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'provinces', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'provinces', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'provinces', + 'active', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'sexes', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'earn_rate', + { + type: Sequelize.DataTypes.DECIMAL, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'date_of_birth', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'phone_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'start_work_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'born_place', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'document_type', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'document_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'emergency_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'current_place', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'shareholders', + 'sex', + { + type: Sequelize.DataTypes.ENUM, + + values: ['M','F'], + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staff_status', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staff_status', + 'css', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'date_of_birth', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'phone_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'start_work_date', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'born_place', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'document_type', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'document_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'emergency_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'current_place', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'statusId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'staff_status', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'sex', + { + type: Sequelize.DataTypes.ENUM, + + values: ['M','F'], + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'staffs', + 'branchId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'branches', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'urls', + 'method', + { + type: Sequelize.DataTypes.ENUM, + + values: ['GET','POST','PATCH','DELETE'], + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'urls', + 'uri', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'urls', + 'route_name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'urls', + 'acitve', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'urls', + 'is_menu', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'user_has_menu', + 'userId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'user_has_menu', + 'menuId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'menus', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'user_has_menu', + 'status', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'user_type_urls', + 'user_typeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'user_types', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'user_type_urls', + 'urlId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'urls', + key: 'id', + }, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'user_types', + 'is_admin', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'user_types', + 'name', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'villages', + 'name_kh', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'villages', + 'name_en', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await queryInterface.addColumn( + 'villages', + 'communeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'communes', + key: 'id', + }, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'villages', + 'communeId', + { transaction } + ); + + await queryInterface.removeColumn( + 'villages', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'villages', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'user_types', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'user_types', + 'is_admin', + { transaction } + ); + + await queryInterface.removeColumn( + 'user_type_urls', + 'urlId', + { transaction } + ); + + await queryInterface.removeColumn( + 'user_type_urls', + 'user_typeId', + { transaction } + ); + + await queryInterface.removeColumn( + 'user_has_menu', + 'status', + { transaction } + ); + + await queryInterface.removeColumn( + 'user_has_menu', + 'menuId', + { transaction } + ); + + await queryInterface.removeColumn( + 'user_has_menu', + 'userId', + { transaction } + ); + + await queryInterface.removeColumn( + 'urls', + 'is_menu', + { transaction } + ); + + await queryInterface.removeColumn( + 'urls', + 'acitve', + { transaction } + ); + + await queryInterface.removeColumn( + 'urls', + 'route_name', + { transaction } + ); + + await queryInterface.removeColumn( + 'urls', + 'uri', + { transaction } + ); + + await queryInterface.removeColumn( + 'urls', + 'method', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'branchId', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'sex', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'statusId', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'current_place', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'emergency_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'document_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'document_type', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'born_place', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'start_work_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'phone_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'date_of_birth', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'staffs', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'staff_status', + 'css', + { transaction } + ); + + await queryInterface.removeColumn( + 'staff_status', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'sex', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'current_place', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'emergency_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'document_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'document_type', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'born_place', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'start_work_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'phone_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'date_of_birth', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'earn_rate', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'shareholders', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'sexes', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'provinces', + 'active', + { transaction } + ); + + await queryInterface.removeColumn( + 'provinces', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'provinces', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'statusId', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'loanId', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'remark', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'cross_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'pending_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'penalty_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'total_paid_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'total_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'commission_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'interest_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'interval', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'deduct_paid_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'deduct_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'sort', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'last_payment_paid_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'payment_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'payments', + 'start_payment_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'paymentId', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'type', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'setlement_datetime', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'revenue_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'commission_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'interest_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'deduct_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'transaction_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_transactions', + 'transaction_datetime', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_status', + 'visible', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_status', + 'css', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_status', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_revenues', + 'branchId', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_revenues', + 'setlement_datetime', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_revenues', + 'expense_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_revenues', + 'commission_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_revenues', + 'interest_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_revenues', + 'admin_fee_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'payment_revenues', + 'transaction_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'groupId', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'urlId', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'icon', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'permission', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'active_url', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'url', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'label', + { transaction } + ); + + await queryInterface.removeColumn( + 'menus', + 'parentId', + { transaction } + ); + + await queryInterface.removeColumn( + 'members', + 'sexId', + { transaction } + ); + + await queryInterface.removeColumn( + 'members', + 'phone_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'members', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'members', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'loan_typeId', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'branchId', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'interest_rateId', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'staffId', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'clientId', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'statusId', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'purpose', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'admin_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'admin_rate', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'finish_discount_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'finish_discount', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'finish_payment_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'last_payment_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'started_payment_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'registration_date', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'commission_rate', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'rate', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'last_pending_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'pending_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'term', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'principal_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'loans', + 'code', + { transaction } + ); + + await queryInterface.removeColumn( + 'loan_types', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'loan_types', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'loan_status', + 'css', + { transaction } + ); + + await queryInterface.removeColumn( + 'loan_status', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'setting', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'css', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'sort', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'interval', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'commission_rate', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'rate', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'interest_rates', + 'code', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'full_address_input', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'villageId', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'communeId', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'districtId', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'provinceId', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'phone_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'document_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'document_type', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'date_of_birth', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'sex', + { transaction } + ); + + await queryInterface.removeColumn( + 'guarantor', + 'full_name', + { transaction } + ); + + await queryInterface.removeColumn( + 'group_menus', + 'is_admin', + { transaction } + ); + + await queryInterface.removeColumn( + 'group_menus', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'expense_types', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'expense_types', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'expense_items', + 'branchId', + { transaction } + ); + + await queryInterface.removeColumn( + 'expense_items', + 'expense_typeId', + { transaction } + ); + + await queryInterface.removeColumn( + 'expense_items', + 'expense_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'expense_items', + 'description', + { transaction } + ); + + await queryInterface.removeColumn( + 'expense_items', + 'expense_datetime', + { transaction } + ); + + await queryInterface.removeColumn( + 'document_type', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'districts', + 'provinceId', + { transaction } + ); + + await queryInterface.removeColumn( + 'districts', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'districts', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'deposits', + 'branchId', + { transaction } + ); + + await queryInterface.removeColumn( + 'deposits', + 'shareholderId', + { transaction } + ); + + await queryInterface.removeColumn( + 'deposits', + 'deposit_amount', + { transaction } + ); + + await queryInterface.removeColumn( + 'deposits', + 'deposit_datetime', + { transaction } + ); + + await queryInterface.removeColumn( + 'communes', + 'districtId', + { transaction } + ); + + await queryInterface.removeColumn( + 'communes', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'communes', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'branchId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'villageId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'communeId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'districtId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'provinceId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'userId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'statusId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'sex', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'document_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'document_typeId', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'is_new', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'phone_number', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'date_of_birth', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'name_kh', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'name_en', + { transaction } + ); + + await queryInterface.removeColumn( + 'clients', + 'code', + { transaction } + ); + + await queryInterface.removeColumn( + 'client_status', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'calendars', + 'flag', + { transaction } + ); + + await queryInterface.removeColumn( + 'calendars', + 'description', + { transaction } + ); + + await queryInterface.removeColumn( + 'calendars', + 'is_holiday', + { transaction } + ); + + await queryInterface.removeColumn( + 'calendars', + 'is_weekend', + { transaction } + ); + + await queryInterface.removeColumn( + 'calendars', + 'date', + { transaction } + ); + + await queryInterface.removeColumn( + 'branches', + 'description', + { transaction } + ); + + await queryInterface.removeColumn( + 'branches', + 'name', + { transaction } + ); + + await queryInterface.removeColumn( + 'branches', + 'code', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'provider', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'passwordResetTokenExpiresAt', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'passwordResetToken', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'emailVerificationTokenExpiresAt', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'emailVerificationToken', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'emailVerified', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'password', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'disabled', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'email', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'phoneNumber', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'lastName', + { transaction } + ); + + await queryInterface.removeColumn( + 'users', + 'firstName', + { transaction } + ); + + await queryInterface.dropTable('villages', { transaction }); + + await queryInterface.dropTable('user_types', { transaction }); + + await queryInterface.dropTable('user_type_urls', { transaction }); + + await queryInterface.dropTable('user_has_menu', { transaction }); + + await queryInterface.dropTable('urls', { transaction }); + + await queryInterface.dropTable('staffs', { transaction }); + + await queryInterface.dropTable('staff_status', { transaction }); + + await queryInterface.dropTable('shareholders', { transaction }); + + await queryInterface.dropTable('sexes', { transaction }); + + await queryInterface.dropTable('provinces', { transaction }); + + await queryInterface.dropTable('payments', { transaction }); + + await queryInterface.dropTable('payment_transactions', { transaction }); + + await queryInterface.dropTable('payment_status', { transaction }); + + await queryInterface.dropTable('payment_revenues', { transaction }); + + await queryInterface.dropTable('menus', { transaction }); + + await queryInterface.dropTable('members', { transaction }); + + await queryInterface.dropTable('loans', { transaction }); + + await queryInterface.dropTable('loan_types', { transaction }); + + await queryInterface.dropTable('loan_status', { transaction }); + + await queryInterface.dropTable('interest_rates', { transaction }); + + await queryInterface.dropTable('guarantor', { transaction }); + + await queryInterface.dropTable('group_menus', { transaction }); + + await queryInterface.dropTable('expense_types', { transaction }); + + await queryInterface.dropTable('expense_items', { transaction }); + + await queryInterface.dropTable('document_type', { transaction }); + + await queryInterface.dropTable('districts', { transaction }); + + await queryInterface.dropTable('deposits', { transaction }); + + await queryInterface.dropTable('communes', { transaction }); + + await queryInterface.dropTable('clients', { transaction }); + + await queryInterface.dropTable('client_status', { transaction }); + + await queryInterface.dropTable('calendars', { transaction }); + + await queryInterface.dropTable('branches', { transaction }); + + await queryInterface.dropTable('users', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/models/branches.js b/backend/src/db/models/branches.js new file mode 100644 index 0000000..e22ab71 --- /dev/null +++ b/backend/src/db/models/branches.js @@ -0,0 +1,58 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const branches = sequelize.define( + 'branches', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +code: { + type: DataTypes.TEXT, + + }, + +name: { + type: DataTypes.TEXT, + + }, + +description: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + branches.associate = (db) => { + + db.branches.belongsTo(db.users, { + as: 'createdBy', + }); + + db.branches.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return branches; +}; + diff --git a/backend/src/db/models/calendars.js b/backend/src/db/models/calendars.js new file mode 100644 index 0000000..5913ccf --- /dev/null +++ b/backend/src/db/models/calendars.js @@ -0,0 +1,74 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const calendars = sequelize.define( + 'calendars', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +date: { + type: DataTypes.DATE, + + }, + +is_weekend: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + +is_holiday: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + +description: { + type: DataTypes.TEXT, + + }, + +flag: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + calendars.associate = (db) => { + + db.calendars.belongsTo(db.users, { + as: 'createdBy', + }); + + db.calendars.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return calendars; +}; + diff --git a/backend/src/db/models/client_status.js b/backend/src/db/models/client_status.js new file mode 100644 index 0000000..b21bd42 --- /dev/null +++ b/backend/src/db/models/client_status.js @@ -0,0 +1,48 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const client_status = sequelize.define( + 'client_status', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + client_status.associate = (db) => { + + db.client_status.belongsTo(db.users, { + as: 'createdBy', + }); + + db.client_status.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return client_status; +}; + diff --git a/backend/src/db/models/clients.js b/backend/src/db/models/clients.js new file mode 100644 index 0000000..fc1d7e8 --- /dev/null +++ b/backend/src/db/models/clients.js @@ -0,0 +1,94 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const clients = sequelize.define( + 'clients', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +code: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +date_of_birth: { + type: DataTypes.DATE, + + }, + +phone_number: { + type: DataTypes.TEXT, + + }, + +is_new: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + +document_number: { + type: DataTypes.TEXT, + + }, + +sex: { + type: DataTypes.ENUM, + + values: [ + +"M", + +"F" + + ], + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + clients.associate = (db) => { + + db.clients.belongsTo(db.users, { + as: 'createdBy', + }); + + db.clients.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return clients; +}; + diff --git a/backend/src/db/models/communes.js b/backend/src/db/models/communes.js new file mode 100644 index 0000000..40f93f1 --- /dev/null +++ b/backend/src/db/models/communes.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const communes = sequelize.define( + 'communes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + communes.associate = (db) => { + + db.communes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.communes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return communes; +}; + diff --git a/backend/src/db/models/deposits.js b/backend/src/db/models/deposits.js new file mode 100644 index 0000000..761c88c --- /dev/null +++ b/backend/src/db/models/deposits.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const deposits = sequelize.define( + 'deposits', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +deposit_datetime: { + type: DataTypes.DATE, + + }, + +deposit_amount: { + type: DataTypes.DECIMAL, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + deposits.associate = (db) => { + + db.deposits.belongsTo(db.users, { + as: 'createdBy', + }); + + db.deposits.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return deposits; +}; + diff --git a/backend/src/db/models/districts.js b/backend/src/db/models/districts.js new file mode 100644 index 0000000..af270e3 --- /dev/null +++ b/backend/src/db/models/districts.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const districts = sequelize.define( + 'districts', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + districts.associate = (db) => { + + db.districts.belongsTo(db.users, { + as: 'createdBy', + }); + + db.districts.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return districts; +}; + diff --git a/backend/src/db/models/document_type.js b/backend/src/db/models/document_type.js new file mode 100644 index 0000000..8f19292 --- /dev/null +++ b/backend/src/db/models/document_type.js @@ -0,0 +1,48 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const document_type = sequelize.define( + 'document_type', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + document_type.associate = (db) => { + + db.document_type.belongsTo(db.users, { + as: 'createdBy', + }); + + db.document_type.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return document_type; +}; + diff --git a/backend/src/db/models/expense_items.js b/backend/src/db/models/expense_items.js new file mode 100644 index 0000000..ebef596 --- /dev/null +++ b/backend/src/db/models/expense_items.js @@ -0,0 +1,58 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const expense_items = sequelize.define( + 'expense_items', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +expense_datetime: { + type: DataTypes.DATE, + + }, + +description: { + type: DataTypes.TEXT, + + }, + +expense_amount: { + type: DataTypes.DECIMAL, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + expense_items.associate = (db) => { + + db.expense_items.belongsTo(db.users, { + as: 'createdBy', + }); + + db.expense_items.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return expense_items; +}; + diff --git a/backend/src/db/models/expense_types.js b/backend/src/db/models/expense_types.js new file mode 100644 index 0000000..e89474f --- /dev/null +++ b/backend/src/db/models/expense_types.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const expense_types = sequelize.define( + 'expense_types', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + expense_types.associate = (db) => { + + db.expense_types.belongsTo(db.users, { + as: 'createdBy', + }); + + db.expense_types.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return expense_types; +}; + diff --git a/backend/src/db/models/group_menus.js b/backend/src/db/models/group_menus.js new file mode 100644 index 0000000..2dc37e0 --- /dev/null +++ b/backend/src/db/models/group_menus.js @@ -0,0 +1,56 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const group_menus = sequelize.define( + 'group_menus', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name: { + type: DataTypes.TEXT, + + }, + +is_admin: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + group_menus.associate = (db) => { + + db.group_menus.belongsTo(db.users, { + as: 'createdBy', + }); + + db.group_menus.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return group_menus; +}; + diff --git a/backend/src/db/models/guarantor.js b/backend/src/db/models/guarantor.js new file mode 100644 index 0000000..b46d766 --- /dev/null +++ b/backend/src/db/models/guarantor.js @@ -0,0 +1,86 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const guarantor = sequelize.define( + 'guarantor', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +full_name: { + type: DataTypes.TEXT, + + }, + +sex: { + type: DataTypes.ENUM, + + values: [ + +"M", + +"F" + + ], + + }, + +date_of_birth: { + type: DataTypes.DATE, + + }, + +document_type: { + type: DataTypes.TEXT, + + }, + +document_number: { + type: DataTypes.TEXT, + + }, + +phone_number: { + type: DataTypes.TEXT, + + }, + +full_address_input: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + guarantor.associate = (db) => { + + db.guarantor.belongsTo(db.users, { + as: 'createdBy', + }); + + db.guarantor.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return guarantor; +}; + diff --git a/backend/src/db/models/index.js b/backend/src/db/models/index.js new file mode 100644 index 0000000..4a3852f --- /dev/null +++ b/backend/src/db/models/index.js @@ -0,0 +1,38 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require("../db.config")[env]; +const db = {}; + +let sequelize; +console.log(env); +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(file => { + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); + }) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes) + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/backend/src/db/models/interest_rates.js b/backend/src/db/models/interest_rates.js new file mode 100644 index 0000000..26c8ac2 --- /dev/null +++ b/backend/src/db/models/interest_rates.js @@ -0,0 +1,83 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const interest_rates = sequelize.define( + 'interest_rates', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +code: { + type: DataTypes.TEXT, + + }, + +name: { + type: DataTypes.TEXT, + + }, + +rate: { + type: DataTypes.DECIMAL, + + }, + +commission_rate: { + type: DataTypes.DECIMAL, + + }, + +interval: { + type: DataTypes.INTEGER, + + }, + +sort: { + type: DataTypes.INTEGER, + + }, + +css: { + type: DataTypes.TEXT, + + }, + +setting: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + interest_rates.associate = (db) => { + + db.interest_rates.belongsTo(db.users, { + as: 'createdBy', + }); + + db.interest_rates.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return interest_rates; +}; + diff --git a/backend/src/db/models/loan_status.js b/backend/src/db/models/loan_status.js new file mode 100644 index 0000000..a6ca5d2 --- /dev/null +++ b/backend/src/db/models/loan_status.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const loan_status = sequelize.define( + 'loan_status', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name: { + type: DataTypes.TEXT, + + }, + +css: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + loan_status.associate = (db) => { + + db.loan_status.belongsTo(db.users, { + as: 'createdBy', + }); + + db.loan_status.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return loan_status; +}; + diff --git a/backend/src/db/models/loan_types.js b/backend/src/db/models/loan_types.js new file mode 100644 index 0000000..80f7320 --- /dev/null +++ b/backend/src/db/models/loan_types.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const loan_types = sequelize.define( + 'loan_types', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + loan_types.associate = (db) => { + + db.loan_types.belongsTo(db.users, { + as: 'createdBy', + }); + + db.loan_types.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return loan_types; +}; + diff --git a/backend/src/db/models/loans.js b/backend/src/db/models/loans.js new file mode 100644 index 0000000..e69b37e --- /dev/null +++ b/backend/src/db/models/loans.js @@ -0,0 +1,123 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const loans = sequelize.define( + 'loans', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +code: { + type: DataTypes.TEXT, + + }, + +principal_amount: { + type: DataTypes.DECIMAL, + + }, + +term: { + type: DataTypes.INTEGER, + + }, + +pending_amount: { + type: DataTypes.DECIMAL, + + }, + +last_pending_amount: { + type: DataTypes.DECIMAL, + + }, + +rate: { + type: DataTypes.DECIMAL, + + }, + +commission_rate: { + type: DataTypes.DECIMAL, + + }, + +registration_date: { + type: DataTypes.DATE, + + }, + +started_payment_date: { + type: DataTypes.DATE, + + }, + +last_payment_date: { + type: DataTypes.DATE, + + }, + +finish_payment_date: { + type: DataTypes.DATE, + + }, + +finish_discount: { + type: DataTypes.DECIMAL, + + }, + +finish_discount_amount: { + type: DataTypes.DECIMAL, + + }, + +admin_rate: { + type: DataTypes.DECIMAL, + + }, + +admin_amount: { + type: DataTypes.DECIMAL, + + }, + +purpose: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + loans.associate = (db) => { + + db.loans.belongsTo(db.users, { + as: 'createdBy', + }); + + db.loans.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return loans; +}; + diff --git a/backend/src/db/models/members.js b/backend/src/db/models/members.js new file mode 100644 index 0000000..a27b1fa --- /dev/null +++ b/backend/src/db/models/members.js @@ -0,0 +1,58 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const members = sequelize.define( + 'members', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + +phone_number: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + members.associate = (db) => { + + db.members.belongsTo(db.users, { + as: 'createdBy', + }); + + db.members.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return members; +}; + diff --git a/backend/src/db/models/menus.js b/backend/src/db/models/menus.js new file mode 100644 index 0000000..8b82b81 --- /dev/null +++ b/backend/src/db/models/menus.js @@ -0,0 +1,68 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const menus = sequelize.define( + 'menus', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +label: { + type: DataTypes.TEXT, + + }, + +url: { + type: DataTypes.TEXT, + + }, + +active_url: { + type: DataTypes.TEXT, + + }, + +permission: { + type: DataTypes.TEXT, + + }, + +icon: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + menus.associate = (db) => { + + db.menus.belongsTo(db.users, { + as: 'createdBy', + }); + + db.menus.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return menus; +}; + diff --git a/backend/src/db/models/payment_revenues.js b/backend/src/db/models/payment_revenues.js new file mode 100644 index 0000000..d9377ee --- /dev/null +++ b/backend/src/db/models/payment_revenues.js @@ -0,0 +1,73 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const payment_revenues = sequelize.define( + 'payment_revenues', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +transaction_date: { + type: DataTypes.DATE, + + }, + +admin_fee_amount: { + type: DataTypes.DECIMAL, + + }, + +interest_amount: { + type: DataTypes.DECIMAL, + + }, + +commission_amount: { + type: DataTypes.DECIMAL, + + }, + +expense_amount: { + type: DataTypes.DECIMAL, + + }, + +setlement_datetime: { + type: DataTypes.DATE, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + payment_revenues.associate = (db) => { + + db.payment_revenues.belongsTo(db.users, { + as: 'createdBy', + }); + + db.payment_revenues.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return payment_revenues; +}; + diff --git a/backend/src/db/models/payment_status.js b/backend/src/db/models/payment_status.js new file mode 100644 index 0000000..67724b0 --- /dev/null +++ b/backend/src/db/models/payment_status.js @@ -0,0 +1,61 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const payment_status = sequelize.define( + 'payment_status', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name: { + type: DataTypes.TEXT, + + }, + +css: { + type: DataTypes.TEXT, + + }, + +visible: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + payment_status.associate = (db) => { + + db.payment_status.belongsTo(db.users, { + as: 'createdBy', + }); + + db.payment_status.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return payment_status; +}; + diff --git a/backend/src/db/models/payment_transactions.js b/backend/src/db/models/payment_transactions.js new file mode 100644 index 0000000..672eb78 --- /dev/null +++ b/backend/src/db/models/payment_transactions.js @@ -0,0 +1,93 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const payment_transactions = sequelize.define( + 'payment_transactions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +transaction_datetime: { + type: DataTypes.DATE, + + }, + +transaction_amount: { + type: DataTypes.DECIMAL, + + }, + +deduct_amount: { + type: DataTypes.DECIMAL, + + }, + +interest_amount: { + type: DataTypes.DECIMAL, + + }, + +commission_amount: { + type: DataTypes.DECIMAL, + + }, + +revenue_amount: { + type: DataTypes.DECIMAL, + + }, + +setlement_datetime: { + type: DataTypes.DATE, + + }, + +type: { + type: DataTypes.ENUM, + + values: [ + +"interest", + +"deduction", + +"reverse" + + ], + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + payment_transactions.associate = (db) => { + + db.payment_transactions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.payment_transactions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return payment_transactions; +}; + diff --git a/backend/src/db/models/payments.js b/backend/src/db/models/payments.js new file mode 100644 index 0000000..cb69451 --- /dev/null +++ b/backend/src/db/models/payments.js @@ -0,0 +1,118 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const payments = sequelize.define( + 'payments', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +start_payment_date: { + type: DataTypes.DATE, + + }, + +payment_date: { + type: DataTypes.DATE, + + }, + +last_payment_paid_date: { + type: DataTypes.DATE, + + }, + +sort: { + type: DataTypes.INTEGER, + + }, + +deduct_amount: { + type: DataTypes.DECIMAL, + + }, + +deduct_paid_amount: { + type: DataTypes.DECIMAL, + + }, + +interval: { + type: DataTypes.INTEGER, + + }, + +interest_amount: { + type: DataTypes.DECIMAL, + + }, + +commission_amount: { + type: DataTypes.DECIMAL, + + }, + +total_amount: { + type: DataTypes.DECIMAL, + + }, + +total_paid_amount: { + type: DataTypes.DECIMAL, + + }, + +penalty_amount: { + type: DataTypes.DECIMAL, + + }, + +pending_amount: { + type: DataTypes.DECIMAL, + + }, + +cross_amount: { + type: DataTypes.DECIMAL, + + }, + +remark: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + payments.associate = (db) => { + + db.payments.belongsTo(db.users, { + as: 'createdBy', + }); + + db.payments.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return payments; +}; + diff --git a/backend/src/db/models/provinces.js b/backend/src/db/models/provinces.js new file mode 100644 index 0000000..a90d34c --- /dev/null +++ b/backend/src/db/models/provinces.js @@ -0,0 +1,61 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const provinces = sequelize.define( + 'provinces', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + +active: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + provinces.associate = (db) => { + + db.provinces.belongsTo(db.users, { + as: 'createdBy', + }); + + db.provinces.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return provinces; +}; + diff --git a/backend/src/db/models/sexes.js b/backend/src/db/models/sexes.js new file mode 100644 index 0000000..34defd8 --- /dev/null +++ b/backend/src/db/models/sexes.js @@ -0,0 +1,48 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const sexes = sequelize.define( + 'sexes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + sexes.associate = (db) => { + + db.sexes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.sexes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return sexes; +}; + diff --git a/backend/src/db/models/shareholders.js b/backend/src/db/models/shareholders.js new file mode 100644 index 0000000..306c340 --- /dev/null +++ b/backend/src/db/models/shareholders.js @@ -0,0 +1,111 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const shareholders = sequelize.define( + 'shareholders', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_en: { + type: DataTypes.TEXT, + + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +earn_rate: { + type: DataTypes.DECIMAL, + + }, + +date_of_birth: { + type: DataTypes.DATE, + + }, + +phone_number: { + type: DataTypes.TEXT, + + }, + +start_work_date: { + type: DataTypes.DATE, + + }, + +born_place: { + type: DataTypes.TEXT, + + }, + +document_type: { + type: DataTypes.TEXT, + + }, + +document_number: { + type: DataTypes.TEXT, + + }, + +emergency_number: { + type: DataTypes.TEXT, + + }, + +current_place: { + type: DataTypes.TEXT, + + }, + +sex: { + type: DataTypes.ENUM, + + values: [ + +"M", + +"F" + + ], + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + shareholders.associate = (db) => { + + db.shareholders.belongsTo(db.users, { + as: 'createdBy', + }); + + db.shareholders.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return shareholders; +}; + diff --git a/backend/src/db/models/staff_status.js b/backend/src/db/models/staff_status.js new file mode 100644 index 0000000..d1ee0ff --- /dev/null +++ b/backend/src/db/models/staff_status.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const staff_status = sequelize.define( + 'staff_status', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name: { + type: DataTypes.TEXT, + + }, + +css: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + staff_status.associate = (db) => { + + db.staff_status.belongsTo(db.users, { + as: 'createdBy', + }); + + db.staff_status.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return staff_status; +}; + diff --git a/backend/src/db/models/staffs.js b/backend/src/db/models/staffs.js new file mode 100644 index 0000000..123f4ef --- /dev/null +++ b/backend/src/db/models/staffs.js @@ -0,0 +1,106 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const staffs = sequelize.define( + 'staffs', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_en: { + type: DataTypes.TEXT, + + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +date_of_birth: { + type: DataTypes.DATE, + + }, + +phone_number: { + type: DataTypes.TEXT, + + }, + +start_work_date: { + type: DataTypes.DATE, + + }, + +born_place: { + type: DataTypes.TEXT, + + }, + +document_type: { + type: DataTypes.TEXT, + + }, + +document_number: { + type: DataTypes.TEXT, + + }, + +emergency_number: { + type: DataTypes.TEXT, + + }, + +current_place: { + type: DataTypes.TEXT, + + }, + +sex: { + type: DataTypes.ENUM, + + values: [ + +"M", + +"F" + + ], + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + staffs.associate = (db) => { + + db.staffs.belongsTo(db.users, { + as: 'createdBy', + }); + + db.staffs.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return staffs; +}; + diff --git a/backend/src/db/models/urls.js b/backend/src/db/models/urls.js new file mode 100644 index 0000000..e13617a --- /dev/null +++ b/backend/src/db/models/urls.js @@ -0,0 +1,86 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const urls = sequelize.define( + 'urls', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +method: { + type: DataTypes.ENUM, + + values: [ + +"GET", + +"POST", + +"PATCH", + +"DELETE" + + ], + + }, + +uri: { + type: DataTypes.TEXT, + + }, + +route_name: { + type: DataTypes.TEXT, + + }, + +acitve: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + +is_menu: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + urls.associate = (db) => { + + db.urls.belongsTo(db.users, { + as: 'createdBy', + }); + + db.urls.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return urls; +}; + diff --git a/backend/src/db/models/user_has_menu.js b/backend/src/db/models/user_has_menu.js new file mode 100644 index 0000000..1cd6f5c --- /dev/null +++ b/backend/src/db/models/user_has_menu.js @@ -0,0 +1,48 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const user_has_menu = sequelize.define( + 'user_has_menu', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +status: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + user_has_menu.associate = (db) => { + + db.user_has_menu.belongsTo(db.users, { + as: 'createdBy', + }); + + db.user_has_menu.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return user_has_menu; +}; + diff --git a/backend/src/db/models/user_type_urls.js b/backend/src/db/models/user_type_urls.js new file mode 100644 index 0000000..4b066bc --- /dev/null +++ b/backend/src/db/models/user_type_urls.js @@ -0,0 +1,43 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const user_type_urls = sequelize.define( + 'user_type_urls', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + user_type_urls.associate = (db) => { + + db.user_type_urls.belongsTo(db.users, { + as: 'createdBy', + }); + + db.user_type_urls.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return user_type_urls; +}; + diff --git a/backend/src/db/models/user_types.js b/backend/src/db/models/user_types.js new file mode 100644 index 0000000..b73d743 --- /dev/null +++ b/backend/src/db/models/user_types.js @@ -0,0 +1,56 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const user_types = sequelize.define( + 'user_types', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +is_admin: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + +name: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + user_types.associate = (db) => { + + db.user_types.belongsTo(db.users, { + as: 'createdBy', + }); + + db.user_types.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return user_types; +}; + diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js new file mode 100644 index 0000000..aad4d4f --- /dev/null +++ b/backend/src/db/models/users.js @@ -0,0 +1,148 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const users = sequelize.define( + 'users', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +firstName: { + type: DataTypes.TEXT, + + }, + +lastName: { + type: DataTypes.TEXT, + + }, + +phoneNumber: { + type: DataTypes.TEXT, + + }, + +email: { + type: DataTypes.TEXT, + + }, + +disabled: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + +password: { + type: DataTypes.TEXT, + + }, + +emailVerified: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + + }, + +emailVerificationToken: { + type: DataTypes.TEXT, + + }, + +emailVerificationTokenExpiresAt: { + type: DataTypes.DATE, + + }, + +passwordResetToken: { + type: DataTypes.TEXT, + + }, + +passwordResetTokenExpiresAt: { + type: DataTypes.DATE, + + }, + +provider: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + users.associate = (db) => { + + db.users.belongsTo(db.users, { + as: 'createdBy', + }); + + db.users.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + users.beforeCreate((users, options) => { + users = trimStringFields(users); + + if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) { + users.emailVerified = true; + + if (!users.password) { + const password = crypto + .randomBytes(20) + .toString('hex'); + + const hashedPassword = bcrypt.hashSync( + password, + config.bcrypt.saltRounds, + ); + + users.password = hashedPassword + } + } + }); + + users.beforeUpdate((users, options) => { + users = trimStringFields(users); + }); + + return users; +}; + +function trimStringFields(users) { + users.email = users.email.trim(); + + users.firstName = users.firstName + ? users.firstName.trim() + : null; + + users.lastName = users.lastName + ? users.lastName.trim() + : null; + + return users; +} + diff --git a/backend/src/db/models/villages.js b/backend/src/db/models/villages.js new file mode 100644 index 0000000..93914f3 --- /dev/null +++ b/backend/src/db/models/villages.js @@ -0,0 +1,53 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const villages = sequelize.define( + 'villages', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +name_kh: { + type: DataTypes.TEXT, + + }, + +name_en: { + type: DataTypes.TEXT, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + villages.associate = (db) => { + + db.villages.belongsTo(db.users, { + as: 'createdBy', + }); + + db.villages.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return villages; +}; + diff --git a/backend/src/db/reset.js b/backend/src/db/reset.js new file mode 100644 index 0000000..5904d4b --- /dev/null +++ b/backend/src/db/reset.js @@ -0,0 +1,16 @@ +const db = require('./models'); +const {execSync} = require("child_process"); + +console.log('Resetting Database'); + +db.sequelize + .sync({ force: true }) + .then(() => { + execSync("sequelize db:seed:all"); + console.log('OK'); + process.exit(); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/backend/src/db/seeders/20200430130759-admin-user.js b/backend/src/db/seeders/20200430130759-admin-user.js new file mode 100644 index 0000000..018685e --- /dev/null +++ b/backend/src/db/seeders/20200430130759-admin-user.js @@ -0,0 +1,66 @@ +'use strict'; +const bcrypt = require("bcrypt"); +const config = require("../../config"); + +const ids = [ + '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af', + 'af5a87be-8f9c-4630-902a-37a60b7005ba', + '5bc531ab-611f-41f3-9373-b7cc5d09c93d', +] + +module.exports = { + up: async (queryInterface, Sequelize) => { + let admin_hash = bcrypt.hashSync(config.admin_pass, config.bcrypt.saltRounds); + let user_hash = bcrypt.hashSync(config.user_pass, config.bcrypt.saltRounds); + + try { + await queryInterface.bulkInsert('users', [ + { + id: ids[0], + firstName: 'Admin', + email: config.admin_email, + emailVerified: true, + provider: config.providers.LOCAL, + password: admin_hash, + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: ids[1], + firstName: 'John', + email: 'john@doe.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: user_hash, + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: ids[2], + firstName: 'Client', + email: 'client@hello.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: user_hash, + createdAt: new Date(), + updatedAt: new Date() + }, + ]); + } catch (error) { + console.error('Error during bulkInsert:', error); + throw error; + } + }, + down: async (queryInterface, Sequelize) => { + try { + await queryInterface.bulkDelete('users', { + id: { + [Sequelize.Op.in]: ids, + }, + }, {}); + } catch (error) { + console.error('Error during bulkDelete:', error); + throw error; + } +} +} diff --git a/backend/src/db/utils.js b/backend/src/db/utils.js new file mode 100644 index 0000000..c253a07 --- /dev/null +++ b/backend/src/db/utils.js @@ -0,0 +1,27 @@ +const validator = require('validator'); +const { v4: uuid } = require('uuid'); +const Sequelize = require('./models').Sequelize; + +module.exports = class Utils { + static uuid(value) { + let id = value; + + if (!validator.isUUID(id)) { + id = uuid(); + } + + return id; + } + + static ilike(model, column, value) { + return Sequelize.where( + Sequelize.fn( + 'lower', + Sequelize.col(`${model}.${column}`), + ), + { + [Sequelize.Op.like]: `%${value}%`.toLowerCase(), + }, + ); + } +}; diff --git a/backend/src/helpers.js b/backend/src/helpers.js new file mode 100644 index 0000000..c38440d --- /dev/null +++ b/backend/src/helpers.js @@ -0,0 +1,23 @@ +const jwt = require('jsonwebtoken'); +const config = require('./config'); + +module.exports = class Helpers { + static wrapAsync(fn) { + return function (req, res, next) { + fn(req, res, next).catch(next); + }; + } + + static commonErrorHandler(error, req, res, next) { + if ([400, 403, 404].includes(error.code)) { + return res.status(error.code).send(error.message); + } + + console.error(error); + return res.status(500).send(error.message); + } + + static jwtSign(data) { + return jwt.sign(data, config.secret_key, {expiresIn: '6h'}); + }; +}; diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..6ff74e1 --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,235 @@ + +const express = require('express'); +const cors = require('cors'); +const app = express(); +const passport = require('passport'); +const path = require('path'); +const fs = require('fs'); +const bodyParser = require('body-parser'); +const db = require('./db/models'); +const config = require('./config'); +const swaggerUI = require('swagger-ui-express'); +const swaggerJsDoc = require('swagger-jsdoc'); + +const authRoutes = require('./routes/auth'); +const searchRoutes = require('./routes/search'); + +const contactFormRoutes = require('./routes/contactForm'); + +const usersRoutes = require('./routes/users'); + +const branchesRoutes = require('./routes/branches'); + +const calendarsRoutes = require('./routes/calendars'); + +const client_statusRoutes = require('./routes/client_status'); + +const clientsRoutes = require('./routes/clients'); + +const communesRoutes = require('./routes/communes'); + +const depositsRoutes = require('./routes/deposits'); + +const districtsRoutes = require('./routes/districts'); + +const document_typeRoutes = require('./routes/document_type'); + +const expense_itemsRoutes = require('./routes/expense_items'); + +const expense_typesRoutes = require('./routes/expense_types'); + +const group_menusRoutes = require('./routes/group_menus'); + +const guarantorRoutes = require('./routes/guarantor'); + +const interest_ratesRoutes = require('./routes/interest_rates'); + +const loan_statusRoutes = require('./routes/loan_status'); + +const loan_typesRoutes = require('./routes/loan_types'); + +const loansRoutes = require('./routes/loans'); + +const membersRoutes = require('./routes/members'); + +const menusRoutes = require('./routes/menus'); + +const payment_revenuesRoutes = require('./routes/payment_revenues'); + +const payment_statusRoutes = require('./routes/payment_status'); + +const payment_transactionsRoutes = require('./routes/payment_transactions'); + +const paymentsRoutes = require('./routes/payments'); + +const provincesRoutes = require('./routes/provinces'); + +const sexesRoutes = require('./routes/sexes'); + +const shareholdersRoutes = require('./routes/shareholders'); + +const staff_statusRoutes = require('./routes/staff_status'); + +const staffsRoutes = require('./routes/staffs'); + +const urlsRoutes = require('./routes/urls'); + +const user_has_menuRoutes = require('./routes/user_has_menu'); + +const user_type_urlsRoutes = require('./routes/user_type_urls'); + +const user_typesRoutes = require('./routes/user_types'); + +const villagesRoutes = require('./routes/villages'); + +const getBaseUrl = (url) => { + if (!url) return ''; + return url.endsWith('/api') ? url.slice(0, -4) : url; +}; + +const options = { + definition: { + openapi: "3.0.0", + info: { + version: "1.0.0", + title: "meng-leap-cash", + description: "meng-leap-cash Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.", + }, + servers: [ + { + url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl, + description: "Development server", + } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + } + }, + responses: { + UnauthorizedError: { + description: "Access token is missing or invalid" + } + } + }, + security: [{ + bearerAuth: [] + }] + }, + apis: ["./src/routes/*.js"], +}; + +const specs = swaggerJsDoc(options); +app.use('/api-docs', function (req, res, next) { + swaggerUI.host = getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host'); + next() + }, swaggerUI.serve, swaggerUI.setup(specs)) + +app.use(cors({origin: true})); +require('./auth/auth'); + +app.use(bodyParser.json()); + +app.use('/api/auth', authRoutes); +app.enable('trust proxy'); + +app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes); + +app.use('/api/branches', passport.authenticate('jwt', {session: false}), branchesRoutes); + +app.use('/api/calendars', passport.authenticate('jwt', {session: false}), calendarsRoutes); + +app.use('/api/client_status', passport.authenticate('jwt', {session: false}), client_statusRoutes); + +app.use('/api/clients', passport.authenticate('jwt', {session: false}), clientsRoutes); + +app.use('/api/communes', passport.authenticate('jwt', {session: false}), communesRoutes); + +app.use('/api/deposits', passport.authenticate('jwt', {session: false}), depositsRoutes); + +app.use('/api/districts', passport.authenticate('jwt', {session: false}), districtsRoutes); + +app.use('/api/document_type', passport.authenticate('jwt', {session: false}), document_typeRoutes); + +app.use('/api/expense_items', passport.authenticate('jwt', {session: false}), expense_itemsRoutes); + +app.use('/api/expense_types', passport.authenticate('jwt', {session: false}), expense_typesRoutes); + +app.use('/api/group_menus', passport.authenticate('jwt', {session: false}), group_menusRoutes); + +app.use('/api/guarantor', passport.authenticate('jwt', {session: false}), guarantorRoutes); + +app.use('/api/interest_rates', passport.authenticate('jwt', {session: false}), interest_ratesRoutes); + +app.use('/api/loan_status', passport.authenticate('jwt', {session: false}), loan_statusRoutes); + +app.use('/api/loan_types', passport.authenticate('jwt', {session: false}), loan_typesRoutes); + +app.use('/api/loans', passport.authenticate('jwt', {session: false}), loansRoutes); + +app.use('/api/members', passport.authenticate('jwt', {session: false}), membersRoutes); + +app.use('/api/menus', passport.authenticate('jwt', {session: false}), menusRoutes); + +app.use('/api/payment_revenues', passport.authenticate('jwt', {session: false}), payment_revenuesRoutes); + +app.use('/api/payment_status', passport.authenticate('jwt', {session: false}), payment_statusRoutes); + +app.use('/api/payment_transactions', passport.authenticate('jwt', {session: false}), payment_transactionsRoutes); + +app.use('/api/payments', passport.authenticate('jwt', {session: false}), paymentsRoutes); + +app.use('/api/provinces', passport.authenticate('jwt', {session: false}), provincesRoutes); + +app.use('/api/sexes', passport.authenticate('jwt', {session: false}), sexesRoutes); + +app.use('/api/shareholders', passport.authenticate('jwt', {session: false}), shareholdersRoutes); + +app.use('/api/staff_status', passport.authenticate('jwt', {session: false}), staff_statusRoutes); + +app.use('/api/staffs', passport.authenticate('jwt', {session: false}), staffsRoutes); + +app.use('/api/urls', passport.authenticate('jwt', {session: false}), urlsRoutes); + +app.use('/api/user_has_menu', passport.authenticate('jwt', {session: false}), user_has_menuRoutes); + +app.use('/api/user_type_urls', passport.authenticate('jwt', {session: false}), user_type_urlsRoutes); + +app.use('/api/user_types', passport.authenticate('jwt', {session: false}), user_typesRoutes); + +app.use('/api/villages', passport.authenticate('jwt', {session: false}), villagesRoutes); + +app.use('/api/contact-form', contactFormRoutes); + +app.use( + '/api/search', + passport.authenticate('jwt', { session: false }), + searchRoutes); + +const publicDir = path.join( + __dirname, + '../public', +); + +if (fs.existsSync(publicDir)) { + app.use('/', express.static(publicDir)); + + app.get('*', function(request, response) { + response.sendFile( + path.resolve(publicDir, 'index.html'), + ); + }); +} + +const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; + +db.sequelize.sync().then(function () { + app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`); + }); +}); + +module.exports = app; diff --git a/backend/src/middlewares/upload.js b/backend/src/middlewares/upload.js new file mode 100644 index 0000000..ea3e835 --- /dev/null +++ b/backend/src/middlewares/upload.js @@ -0,0 +1,11 @@ +const util = require('util'); +const Multer = require('multer'); +const maxSize = 10 * 1024 * 1024; + +let processFile = Multer({ + storage: Multer.memoryStorage(), + limits: { fileSize: maxSize }, +}).single("file"); + +let processFileMiddleware = util.promisify(processFile); +module.exports = processFileMiddleware; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js new file mode 100644 index 0000000..84d08d1 --- /dev/null +++ b/backend/src/routes/auth.js @@ -0,0 +1,200 @@ +const express = require('express'); +const passport = require('passport'); + +const config = require('../config'); +const AuthService = require('../services/auth'); +const ForbiddenError = require('../services/notifications/errors/forbidden'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Auth: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * default: admin@flatlogic.com + * description: User email + * password: + * type: string + * default: password + * description: User password + */ + +/** + * @swagger + * tags: + * name: Auth + * description: Authorization operations + */ + +/** + * @swagger + * /api/auth/signin/local: + * post: + * tags: [Auth] + * summary: Logs user into the system + * description: Logs user into the system + * requestBody: + * description: Set valid user email and password + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Auth" + * responses: + * 200: + * description: Successful login + * 400: + * description: Invalid username/password supplied + * x-codegen-request-body-name: body + */ + +router.post('/signin/local', wrapAsync(async (req, res) => { + const payload = await AuthService.signin(req.body.email, req.body.password, req,); + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/auth/me: + * get: + * security: + * - bearerAuth: [] + * tags: [Auth] + * summary: Get current authorized user info + * description: Get current authorized user info + * responses: + * 200: + * description: Successful retrieval of current authorized user data + * 400: + * description: Invalid username/password supplied + * x-codegen-request-body-name: body + */ + +router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } + + const payload = req.currentUser; + delete payload.password; + res.status(200).send(payload); +}); + +router.put('/password-reset', wrapAsync(async (req, res) => { + const payload = await AuthService.passwordReset(req.body.token, req.body.password, req,); + res.status(200).send(payload); +})); + +router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { + const payload = await AuthService.passwordUpdate(req.body.currentPassword, req.body.newPassword, req); + res.status(200).send(payload); +})); + +router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { + if (!req.currentUser) { + throw new ForbiddenError(); + } + + await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email); + const payload = true; + res.status(200).send(payload); +})); + +router.post('/send-password-reset-email', wrapAsync(async (req, res) => { + const link = new URL(req.headers.referer); + await AuthService.sendPasswordResetEmail(req.body.email, 'register', link.host,); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/auth/signup: + * post: + * tags: [Auth] + * summary: Register new user into the system + * description: Register new user into the system + * requestBody: + * description: Set valid user email and password + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Auth" + * responses: + * 200: + * description: New user successfully signed up + * 400: + * description: Invalid username/password supplied + * 500: + * description: Some server error + * x-codegen-request-body-name: body + */ + +router.post('/signup', wrapAsync(async (req, res) => { + const link = new URL(req.headers.referer); + const payload = await AuthService.signup( + req.body.email, + req.body.password, + req, + link.host, + ) + res.status(200).send(payload); +})); + +router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } + + await AuthService.updateProfile(req.body.profile, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +router.put('/verify-email', wrapAsync(async (req, res) => { + const payload = await AuthService.verifyEmail(req.body.token, req, req.headers.referer) + res.status(200).send(payload); +})); + +router.get('/signin/google', (req, res, next) => { + passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app})(req, res, next); +}); + +router.get('/signin/google/callback', passport.authenticate("google", {failureRedirect: "/login", session: false}), + + function (req, res) { + socialRedirect(res, req.query.state, req.user.token, config); + } +); + +router.get('/signin/microsoft', (req, res, next) => { + passport.authenticate("microsoft", { + scope: ["https://graph.microsoft.com/user.read openid"], + state: req.query.app + })(req, res, next); +}); + +router.get('/signin/microsoft/callback', passport.authenticate("microsoft", { + failureRedirect: "/login", + session: false + }), + function (req, res) { + socialRedirect(res, req.query.state, req.user.token, config); + } +); + +router.use('/', require('../helpers').commonErrorHandler); + +function socialRedirect(res, state, token, config) { + res.redirect(config.uiUrl + "/login?token=" + token); +} + +module.exports = router; diff --git a/backend/src/routes/branches.js b/backend/src/routes/branches.js new file mode 100644 index 0000000..ebf8de9 --- /dev/null +++ b/backend/src/routes/branches.js @@ -0,0 +1,416 @@ + +const express = require('express'); + +const BranchesService = require('../services/branches'); +const BranchesDBApi = require('../db/api/branches'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Branches: + * type: object + * properties: + + * code: + * type: string + * default: code + * name: + * type: string + * default: name + * description: + * type: string + * default: description + + */ + +/** + * @swagger + * tags: + * name: Branches + * description: The Branches managing API + */ + +/** +* @swagger +* /api/branches: +* post: +* security: +* - bearerAuth: [] +* tags: [Branches] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Branches" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Branches" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await BranchesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Branches" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Branches" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await BranchesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/branches/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Branches" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Branches" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await BranchesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/branches/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Branches" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await BranchesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/branches/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Branches" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await BranchesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/branches: + * get: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Get all branches + * description: Get all branches + * responses: + * 200: + * description: Branches list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Branches" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await BranchesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','code','name','description', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/branches/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Count all branches + * description: Count all branches + * responses: + * 200: + * description: Branches count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Branches" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await BranchesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/branches/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Find all branches that match search criteria + * description: Find all branches that match search criteria + * responses: + * 200: + * description: Branches list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Branches" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await BranchesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/branches/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Branches] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Branches" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await BranchesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/calendars.js b/backend/src/routes/calendars.js new file mode 100644 index 0000000..91d8168 --- /dev/null +++ b/backend/src/routes/calendars.js @@ -0,0 +1,414 @@ + +const express = require('express'); + +const CalendarsService = require('../services/calendars'); +const CalendarsDBApi = require('../db/api/calendars'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Calendars: + * type: object + * properties: + + * description: + * type: string + * default: description + * flag: + * type: string + * default: flag + + */ + +/** + * @swagger + * tags: + * name: Calendars + * description: The Calendars managing API + */ + +/** +* @swagger +* /api/calendars: +* post: +* security: +* - bearerAuth: [] +* tags: [Calendars] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Calendars" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Calendars" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await CalendarsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Calendars" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Calendars" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await CalendarsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/calendars/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Calendars" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Calendars" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await CalendarsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/calendars/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Calendars" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await CalendarsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/calendars/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Calendars" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await CalendarsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/calendars: + * get: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Get all calendars + * description: Get all calendars + * responses: + * 200: + * description: Calendars list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Calendars" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await CalendarsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','description','flag', + + 'date', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/calendars/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Count all calendars + * description: Count all calendars + * responses: + * 200: + * description: Calendars count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Calendars" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await CalendarsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/calendars/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Find all calendars that match search criteria + * description: Find all calendars that match search criteria + * responses: + * 200: + * description: Calendars list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Calendars" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await CalendarsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/calendars/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Calendars] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Calendars" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await CalendarsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/client_status.js b/backend/src/routes/client_status.js new file mode 100644 index 0000000..aa8c185 --- /dev/null +++ b/backend/src/routes/client_status.js @@ -0,0 +1,410 @@ + +const express = require('express'); + +const Client_statusService = require('../services/client_status'); +const Client_statusDBApi = require('../db/api/client_status'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Client_status: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Client_status + * description: The Client_status managing API + */ + +/** +* @swagger +* /api/client_status: +* post: +* security: +* - bearerAuth: [] +* tags: [Client_status] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Client_status" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Client_status" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Client_statusService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Client_status" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Client_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Client_statusService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/client_status/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Client_status" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Client_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Client_statusService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/client_status/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Client_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Client_statusService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/client_status/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Client_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Client_statusService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/client_status: + * get: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Get all client_status + * description: Get all client_status + * responses: + * 200: + * description: Client_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Client_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Client_statusDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/client_status/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Count all client_status + * description: Count all client_status + * responses: + * 200: + * description: Client_status count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Client_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Client_statusDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/client_status/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Find all client_status that match search criteria + * description: Find all client_status that match search criteria + * responses: + * 200: + * description: Client_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Client_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Client_statusDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/client_status/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Client_status] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Client_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Client_statusDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/clients.js b/backend/src/routes/clients.js new file mode 100644 index 0000000..2dbf34f --- /dev/null +++ b/backend/src/routes/clients.js @@ -0,0 +1,424 @@ + +const express = require('express'); + +const ClientsService = require('../services/clients'); +const ClientsDBApi = require('../db/api/clients'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Clients: + * type: object + * properties: + + * code: + * type: string + * default: code + * name_en: + * type: string + * default: name_en + * name_kh: + * type: string + * default: name_kh + * phone_number: + * type: string + * default: phone_number + * document_number: + * type: string + * default: document_number + + * + */ + +/** + * @swagger + * tags: + * name: Clients + * description: The Clients managing API + */ + +/** +* @swagger +* /api/clients: +* post: +* security: +* - bearerAuth: [] +* tags: [Clients] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Clients" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Clients" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ClientsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Clients" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Clients" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ClientsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/clients/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Clients" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Clients" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await ClientsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/clients/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Clients" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await ClientsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/clients/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Clients" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await ClientsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/clients: + * get: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Get all clients + * description: Get all clients + * responses: + * 200: + * description: Clients list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Clients" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await ClientsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','code','name_en','name_kh','phone_number','document_number', + + 'date_of_birth', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/clients/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Count all clients + * description: Count all clients + * responses: + * 200: + * description: Clients count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Clients" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await ClientsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/clients/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Find all clients that match search criteria + * description: Find all clients that match search criteria + * responses: + * 200: + * description: Clients list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Clients" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ClientsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/clients/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Clients] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Clients" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await ClientsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/communes.js b/backend/src/routes/communes.js new file mode 100644 index 0000000..bdc51a7 --- /dev/null +++ b/backend/src/routes/communes.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const CommunesService = require('../services/communes'); +const CommunesDBApi = require('../db/api/communes'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Communes: + * type: object + * properties: + + * name_kh: + * type: string + * default: name_kh + * name_en: + * type: string + * default: name_en + + */ + +/** + * @swagger + * tags: + * name: Communes + * description: The Communes managing API + */ + +/** +* @swagger +* /api/communes: +* post: +* security: +* - bearerAuth: [] +* tags: [Communes] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Communes" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Communes" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await CommunesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Communes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Communes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await CommunesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/communes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Communes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Communes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await CommunesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/communes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Communes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await CommunesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/communes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Communes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await CommunesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/communes: + * get: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Get all communes + * description: Get all communes + * responses: + * 200: + * description: Communes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Communes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await CommunesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_kh','name_en', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/communes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Count all communes + * description: Count all communes + * responses: + * 200: + * description: Communes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Communes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await CommunesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/communes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Find all communes that match search criteria + * description: Find all communes that match search criteria + * responses: + * 200: + * description: Communes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Communes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await CommunesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/communes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Communes] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Communes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await CommunesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/contactForm.js b/backend/src/routes/contactForm.js new file mode 100644 index 0000000..3f4da9b --- /dev/null +++ b/backend/src/routes/contactForm.js @@ -0,0 +1,22 @@ + +const express = require('express'); +const router = express.Router(); + +router.post('/send', async (req, res) => { + try { + const { email, subject, message } = req.body; + + if (!email || !subject || !message) { + return res.status(400).json({ error: 'All fields are required' }); + } + + // Generate the email content + // Send the email + res.status(200).json({ message: 'Email sent successfully' }); + } catch (error) { + console.error('Error sending email:', error); + res.status(500).json({ error: 'Error sending email' }); + } + }); + + module.exports = router; diff --git a/backend/src/routes/deposits.js b/backend/src/routes/deposits.js new file mode 100644 index 0000000..0099006 --- /dev/null +++ b/backend/src/routes/deposits.js @@ -0,0 +1,412 @@ + +const express = require('express'); + +const DepositsService = require('../services/deposits'); +const DepositsDBApi = require('../db/api/deposits'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Deposits: + * type: object + * properties: + + * deposit_amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Deposits + * description: The Deposits managing API + */ + +/** +* @swagger +* /api/deposits: +* post: +* security: +* - bearerAuth: [] +* tags: [Deposits] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Deposits" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Deposits" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await DepositsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Deposits" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Deposits" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await DepositsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/deposits/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Deposits" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Deposits" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await DepositsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/deposits/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Deposits" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await DepositsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/deposits/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Deposits" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await DepositsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/deposits: + * get: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Get all deposits + * description: Get all deposits + * responses: + * 200: + * description: Deposits list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Deposits" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await DepositsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id', + + 'deposit_amount', + 'deposit_datetime', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/deposits/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Count all deposits + * description: Count all deposits + * responses: + * 200: + * description: Deposits count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Deposits" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await DepositsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/deposits/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Find all deposits that match search criteria + * description: Find all deposits that match search criteria + * responses: + * 200: + * description: Deposits list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Deposits" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await DepositsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/deposits/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Deposits] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Deposits" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await DepositsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/districts.js b/backend/src/routes/districts.js new file mode 100644 index 0000000..f211670 --- /dev/null +++ b/backend/src/routes/districts.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const DistrictsService = require('../services/districts'); +const DistrictsDBApi = require('../db/api/districts'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Districts: + * type: object + * properties: + + * name_kh: + * type: string + * default: name_kh + * name_en: + * type: string + * default: name_en + + */ + +/** + * @swagger + * tags: + * name: Districts + * description: The Districts managing API + */ + +/** +* @swagger +* /api/districts: +* post: +* security: +* - bearerAuth: [] +* tags: [Districts] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Districts" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Districts" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await DistrictsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Districts" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Districts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await DistrictsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/districts/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Districts" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Districts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await DistrictsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/districts/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Districts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await DistrictsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/districts/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Districts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await DistrictsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/districts: + * get: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Get all districts + * description: Get all districts + * responses: + * 200: + * description: Districts list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Districts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await DistrictsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_kh','name_en', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/districts/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Count all districts + * description: Count all districts + * responses: + * 200: + * description: Districts count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Districts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await DistrictsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/districts/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Find all districts that match search criteria + * description: Find all districts that match search criteria + * responses: + * 200: + * description: Districts list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Districts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await DistrictsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/districts/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Districts] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Districts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await DistrictsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/document_type.js b/backend/src/routes/document_type.js new file mode 100644 index 0000000..3600e75 --- /dev/null +++ b/backend/src/routes/document_type.js @@ -0,0 +1,410 @@ + +const express = require('express'); + +const Document_typeService = require('../services/document_type'); +const Document_typeDBApi = require('../db/api/document_type'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Document_type: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Document_type + * description: The Document_type managing API + */ + +/** +* @swagger +* /api/document_type: +* post: +* security: +* - bearerAuth: [] +* tags: [Document_type] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Document_type" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Document_type" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Document_typeService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Document_type" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Document_type" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Document_typeService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/document_type/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Document_type" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Document_type" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Document_typeService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/document_type/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Document_type" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Document_typeService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/document_type/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Document_type" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Document_typeService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/document_type: + * get: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Get all document_type + * description: Get all document_type + * responses: + * 200: + * description: Document_type list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Document_type" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Document_typeDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/document_type/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Count all document_type + * description: Count all document_type + * responses: + * 200: + * description: Document_type count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Document_type" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Document_typeDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/document_type/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Find all document_type that match search criteria + * description: Find all document_type that match search criteria + * responses: + * 200: + * description: Document_type list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Document_type" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Document_typeDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/document_type/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Document_type] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Document_type" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Document_typeDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/expense_items.js b/backend/src/routes/expense_items.js new file mode 100644 index 0000000..e72c49b --- /dev/null +++ b/backend/src/routes/expense_items.js @@ -0,0 +1,416 @@ + +const express = require('express'); + +const Expense_itemsService = require('../services/expense_items'); +const Expense_itemsDBApi = require('../db/api/expense_items'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Expense_items: + * type: object + * properties: + + * description: + * type: string + * default: description + + * expense_amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Expense_items + * description: The Expense_items managing API + */ + +/** +* @swagger +* /api/expense_items: +* post: +* security: +* - bearerAuth: [] +* tags: [Expense_items] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Expense_items" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Expense_items" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Expense_itemsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Expense_items" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Expense_itemsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_items/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Expense_items" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_items" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Expense_itemsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_items/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_items" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Expense_itemsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_items/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Expense_itemsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/expense_items: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Get all expense_items + * description: Get all expense_items + * responses: + * 200: + * description: Expense_items list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Expense_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Expense_itemsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','description', + + 'expense_amount', + 'expense_datetime', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/expense_items/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Count all expense_items + * description: Count all expense_items + * responses: + * 200: + * description: Expense_items count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Expense_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Expense_itemsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_items/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Find all expense_items that match search criteria + * description: Find all expense_items that match search criteria + * responses: + * 200: + * description: Expense_items list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Expense_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Expense_itemsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/expense_items/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_items] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_items" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Expense_itemsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/expense_types.js b/backend/src/routes/expense_types.js new file mode 100644 index 0000000..fc98cfb --- /dev/null +++ b/backend/src/routes/expense_types.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const Expense_typesService = require('../services/expense_types'); +const Expense_typesDBApi = require('../db/api/expense_types'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Expense_types: + * type: object + * properties: + + * name_kh: + * type: string + * default: name_kh + * name_en: + * type: string + * default: name_en + + */ + +/** + * @swagger + * tags: + * name: Expense_types + * description: The Expense_types managing API + */ + +/** +* @swagger +* /api/expense_types: +* post: +* security: +* - bearerAuth: [] +* tags: [Expense_types] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Expense_types" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Expense_types" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Expense_typesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Expense_types" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Expense_typesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_types/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Expense_types" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Expense_typesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_types/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Expense_typesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_types/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Expense_typesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/expense_types: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Get all expense_types + * description: Get all expense_types + * responses: + * 200: + * description: Expense_types list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Expense_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Expense_typesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_kh','name_en', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/expense_types/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Count all expense_types + * description: Count all expense_types + * responses: + * 200: + * description: Expense_types count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Expense_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Expense_typesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/expense_types/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Find all expense_types that match search criteria + * description: Find all expense_types that match search criteria + * responses: + * 200: + * description: Expense_types list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Expense_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Expense_typesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/expense_types/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Expense_types] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Expense_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Expense_typesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/file.js b/backend/src/routes/file.js new file mode 100644 index 0000000..ddd2bc0 --- /dev/null +++ b/backend/src/routes/file.js @@ -0,0 +1,32 @@ +const express = require('express'); +const config = require('../config'); +const path = require('path'); +const passport = require('passport'); +const services = require('../services/file'); +const router = express.Router(); + +router.get('/download', (req, res) => { + if (process.env.NODE_ENV == "production" || process.env.NEXT_PUBLIC_BACK_API) { + services.downloadGCloud(req, res); + } + else { + services.downloadLocal(req, res); + } +}); + +router.post('/upload/:table/:field', passport.authenticate('jwt', {session: false}), (req, res) => { + const fileName = `${req.params.table}/${req.params.field}`; + + if (process.env.NODE_ENV == "production" || process.env.NEXT_PUBLIC_BACK_API) { + services.uploadGCloud(fileName, req, res); + } + else { + services.uploadLocal(fileName, { + entity: null, + maxFileSize: 10 * 1024 * 1024, + folderIncludesAuthenticationUid: false, + })(req, res); + } +}); + +module.exports = router; diff --git a/backend/src/routes/group_menus.js b/backend/src/routes/group_menus.js new file mode 100644 index 0000000..1d3128a --- /dev/null +++ b/backend/src/routes/group_menus.js @@ -0,0 +1,410 @@ + +const express = require('express'); + +const Group_menusService = require('../services/group_menus'); +const Group_menusDBApi = require('../db/api/group_menus'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Group_menus: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Group_menus + * description: The Group_menus managing API + */ + +/** +* @swagger +* /api/group_menus: +* post: +* security: +* - bearerAuth: [] +* tags: [Group_menus] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Group_menus" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Group_menus" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Group_menusService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Group_menus" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Group_menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Group_menusService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/group_menus/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Group_menus" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Group_menus" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Group_menusService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/group_menus/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Group_menus" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Group_menusService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/group_menus/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Group_menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Group_menusService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/group_menus: + * get: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Get all group_menus + * description: Get all group_menus + * responses: + * 200: + * description: Group_menus list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Group_menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Group_menusDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/group_menus/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Count all group_menus + * description: Count all group_menus + * responses: + * 200: + * description: Group_menus count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Group_menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Group_menusDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/group_menus/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Find all group_menus that match search criteria + * description: Find all group_menus that match search criteria + * responses: + * 200: + * description: Group_menus list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Group_menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Group_menusDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/group_menus/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Group_menus] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Group_menus" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Group_menusDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/guarantor.js b/backend/src/routes/guarantor.js new file mode 100644 index 0000000..0010fbd --- /dev/null +++ b/backend/src/routes/guarantor.js @@ -0,0 +1,424 @@ + +const express = require('express'); + +const GuarantorService = require('../services/guarantor'); +const GuarantorDBApi = require('../db/api/guarantor'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Guarantor: + * type: object + * properties: + + * full_name: + * type: string + * default: full_name + * document_type: + * type: string + * default: document_type + * document_number: + * type: string + * default: document_number + * phone_number: + * type: string + * default: phone_number + * full_address_input: + * type: string + * default: full_address_input + + * + */ + +/** + * @swagger + * tags: + * name: Guarantor + * description: The Guarantor managing API + */ + +/** +* @swagger +* /api/guarantor: +* post: +* security: +* - bearerAuth: [] +* tags: [Guarantor] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Guarantor" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Guarantor" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await GuarantorService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Guarantor" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Guarantor" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await GuarantorService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/guarantor/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Guarantor" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Guarantor" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await GuarantorService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/guarantor/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Guarantor" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await GuarantorService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/guarantor/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Guarantor" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await GuarantorService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/guarantor: + * get: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Get all guarantor + * description: Get all guarantor + * responses: + * 200: + * description: Guarantor list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Guarantor" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await GuarantorDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','full_name','document_type','document_number','phone_number','full_address_input', + + 'date_of_birth', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/guarantor/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Count all guarantor + * description: Count all guarantor + * responses: + * 200: + * description: Guarantor count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Guarantor" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await GuarantorDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/guarantor/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Find all guarantor that match search criteria + * description: Find all guarantor that match search criteria + * responses: + * 200: + * description: Guarantor list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Guarantor" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await GuarantorDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/guarantor/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Guarantor] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Guarantor" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await GuarantorDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/interest_rates.js b/backend/src/routes/interest_rates.js new file mode 100644 index 0000000..ef7f99d --- /dev/null +++ b/backend/src/routes/interest_rates.js @@ -0,0 +1,435 @@ + +const express = require('express'); + +const Interest_ratesService = require('../services/interest_rates'); +const Interest_ratesDBApi = require('../db/api/interest_rates'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Interest_rates: + * type: object + * properties: + + * code: + * type: string + * default: code + * name: + * type: string + * default: name + * css: + * type: string + * default: css + * setting: + * type: string + * default: setting + + * interval: + * type: integer + * format: int64 + * sort: + * type: integer + * format: int64 + + * rate: + * type: integer + * format: int64 + * commission_rate: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Interest_rates + * description: The Interest_rates managing API + */ + +/** +* @swagger +* /api/interest_rates: +* post: +* security: +* - bearerAuth: [] +* tags: [Interest_rates] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Interest_rates" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Interest_rates" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Interest_ratesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Interest_rates" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Interest_rates" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Interest_ratesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/interest_rates/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Interest_rates" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Interest_rates" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Interest_ratesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/interest_rates/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Interest_rates" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Interest_ratesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/interest_rates/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Interest_rates" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Interest_ratesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/interest_rates: + * get: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Get all interest_rates + * description: Get all interest_rates + * responses: + * 200: + * description: Interest_rates list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Interest_rates" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Interest_ratesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','code','name','css','setting', + 'interval','sort', + 'rate','commission_rate', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/interest_rates/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Count all interest_rates + * description: Count all interest_rates + * responses: + * 200: + * description: Interest_rates count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Interest_rates" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Interest_ratesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/interest_rates/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Find all interest_rates that match search criteria + * description: Find all interest_rates that match search criteria + * responses: + * 200: + * description: Interest_rates list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Interest_rates" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Interest_ratesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/interest_rates/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Interest_rates] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Interest_rates" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Interest_ratesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/loan_status.js b/backend/src/routes/loan_status.js new file mode 100644 index 0000000..36b73dd --- /dev/null +++ b/backend/src/routes/loan_status.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const Loan_statusService = require('../services/loan_status'); +const Loan_statusDBApi = require('../db/api/loan_status'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Loan_status: + * type: object + * properties: + + * name: + * type: string + * default: name + * css: + * type: string + * default: css + + */ + +/** + * @swagger + * tags: + * name: Loan_status + * description: The Loan_status managing API + */ + +/** +* @swagger +* /api/loan_status: +* post: +* security: +* - bearerAuth: [] +* tags: [Loan_status] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Loan_status" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Loan_status" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Loan_statusService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Loan_status" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Loan_statusService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_status/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Loan_status" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Loan_statusService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_status/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Loan_statusService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_status/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Loan_statusService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/loan_status: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Get all loan_status + * description: Get all loan_status + * responses: + * 200: + * description: Loan_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loan_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Loan_statusDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name','css', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/loan_status/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Count all loan_status + * description: Count all loan_status + * responses: + * 200: + * description: Loan_status count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loan_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Loan_statusDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_status/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Find all loan_status that match search criteria + * description: Find all loan_status that match search criteria + * responses: + * 200: + * description: Loan_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loan_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Loan_statusDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/loan_status/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_status] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Loan_statusDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/loan_types.js b/backend/src/routes/loan_types.js new file mode 100644 index 0000000..a6a50f8 --- /dev/null +++ b/backend/src/routes/loan_types.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const Loan_typesService = require('../services/loan_types'); +const Loan_typesDBApi = require('../db/api/loan_types'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Loan_types: + * type: object + * properties: + + * name_kh: + * type: string + * default: name_kh + * name_en: + * type: string + * default: name_en + + */ + +/** + * @swagger + * tags: + * name: Loan_types + * description: The Loan_types managing API + */ + +/** +* @swagger +* /api/loan_types: +* post: +* security: +* - bearerAuth: [] +* tags: [Loan_types] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Loan_types" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Loan_types" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Loan_typesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Loan_types" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Loan_typesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_types/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Loan_types" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Loan_typesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_types/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Loan_typesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_types/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Loan_typesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/loan_types: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Get all loan_types + * description: Get all loan_types + * responses: + * 200: + * description: Loan_types list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loan_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Loan_typesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_kh','name_en', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/loan_types/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Count all loan_types + * description: Count all loan_types + * responses: + * 200: + * description: Loan_types count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loan_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Loan_typesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loan_types/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Find all loan_types that match search criteria + * description: Find all loan_types that match search criteria + * responses: + * 200: + * description: Loan_types list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loan_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Loan_typesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/loan_types/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Loan_types] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loan_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Loan_typesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/loans.js b/backend/src/routes/loans.js new file mode 100644 index 0000000..a89d7fc --- /dev/null +++ b/backend/src/routes/loans.js @@ -0,0 +1,447 @@ + +const express = require('express'); + +const LoansService = require('../services/loans'); +const LoansDBApi = require('../db/api/loans'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Loans: + * type: object + * properties: + + * code: + * type: string + * default: code + * purpose: + * type: string + * default: purpose + + * term: + * type: integer + * format: int64 + + * principal_amount: + * type: integer + * format: int64 + * pending_amount: + * type: integer + * format: int64 + * last_pending_amount: + * type: integer + * format: int64 + * rate: + * type: integer + * format: int64 + * commission_rate: + * type: integer + * format: int64 + * finish_discount: + * type: integer + * format: int64 + * finish_discount_amount: + * type: integer + * format: int64 + * admin_rate: + * type: integer + * format: int64 + * admin_amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Loans + * description: The Loans managing API + */ + +/** +* @swagger +* /api/loans: +* post: +* security: +* - bearerAuth: [] +* tags: [Loans] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Loans" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Loans" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await LoansService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Loans" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loans" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await LoansService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loans/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Loans" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loans" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await LoansService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loans/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loans" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await LoansService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loans/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loans" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await LoansService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/loans: + * get: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Get all loans + * description: Get all loans + * responses: + * 200: + * description: Loans list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loans" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await LoansDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','code','purpose', + 'term', + 'principal_amount','pending_amount','last_pending_amount','rate','commission_rate','finish_discount','finish_discount_amount','admin_rate','admin_amount', + 'registration_date','started_payment_date','last_payment_date','finish_payment_date', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/loans/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Count all loans + * description: Count all loans + * responses: + * 200: + * description: Loans count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loans" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await LoansDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/loans/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Find all loans that match search criteria + * description: Find all loans that match search criteria + * responses: + * 200: + * description: Loans list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loans" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await LoansDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/loans/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Loans] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loans" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await LoansDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/members.js b/backend/src/routes/members.js new file mode 100644 index 0000000..9970509 --- /dev/null +++ b/backend/src/routes/members.js @@ -0,0 +1,416 @@ + +const express = require('express'); + +const MembersService = require('../services/members'); +const MembersDBApi = require('../db/api/members'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Members: + * type: object + * properties: + + * name_kh: + * type: string + * default: name_kh + * name_en: + * type: string + * default: name_en + * phone_number: + * type: string + * default: phone_number + + */ + +/** + * @swagger + * tags: + * name: Members + * description: The Members managing API + */ + +/** +* @swagger +* /api/members: +* post: +* security: +* - bearerAuth: [] +* tags: [Members] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Members" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Members" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await MembersService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Members" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Members" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await MembersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/members/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Members" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Members" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await MembersService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/members/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Members" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await MembersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/members/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Members" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await MembersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/members: + * get: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Get all members + * description: Get all members + * responses: + * 200: + * description: Members list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Members" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await MembersDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_kh','name_en','phone_number', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/members/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Count all members + * description: Count all members + * responses: + * 200: + * description: Members count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Members" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await MembersDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/members/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Find all members that match search criteria + * description: Find all members that match search criteria + * responses: + * 200: + * description: Members list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Members" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await MembersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/members/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Members] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Members" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await MembersDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/menus.js b/backend/src/routes/menus.js new file mode 100644 index 0000000..814e102 --- /dev/null +++ b/backend/src/routes/menus.js @@ -0,0 +1,422 @@ + +const express = require('express'); + +const MenusService = require('../services/menus'); +const MenusDBApi = require('../db/api/menus'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Menus: + * type: object + * properties: + + * label: + * type: string + * default: label + * url: + * type: string + * default: url + * active_url: + * type: string + * default: active_url + * permission: + * type: string + * default: permission + * icon: + * type: string + * default: icon + + */ + +/** + * @swagger + * tags: + * name: Menus + * description: The Menus managing API + */ + +/** +* @swagger +* /api/menus: +* post: +* security: +* - bearerAuth: [] +* tags: [Menus] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Menus" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Menus" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await MenusService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Menus" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await MenusService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/menus/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Menus" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Menus" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await MenusService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/menus/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Menus" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await MenusService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/menus/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await MenusService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/menus: + * get: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Get all menus + * description: Get all menus + * responses: + * 200: + * description: Menus list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await MenusDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','label','url','active_url','permission','icon', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/menus/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Count all menus + * description: Count all menus + * responses: + * 200: + * description: Menus count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await MenusDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/menus/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Find all menus that match search criteria + * description: Find all menus that match search criteria + * responses: + * 200: + * description: Menus list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Menus" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await MenusDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/menus/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Menus] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Menus" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await MenusDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/openai.js b/backend/src/routes/openai.js new file mode 100644 index 0000000..6fa17ba --- /dev/null +++ b/backend/src/routes/openai.js @@ -0,0 +1,251 @@ +const express = require('express'); +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const sjs = require('sequelize-json-schema'); +const { getWidget, askGpt } = require('../services/openai'); +const RolesService = require('../services/roles'); +const RolesDBApi = require("../db/api/roles"); + +/** + * @swagger + * /api/roles/roles-info/{infoId}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Remove role information by ID + * description: Remove specific role information by ID + * parameters: + * - in: path + * name: infoId + * description: ID of role information to remove + * required: true + * schema: + * type: string + * - in: query + * name: userId + * description: ID of the user + * required: true + * schema: + * type: string + * - in: query + * name: key + * description: Key of the role information to remove + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Role information successfully removed + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: string + * description: The user information + * 400: + * description: Invalid ID or key supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Role not found + * 500: + * description: Some server error + */ + +router.delete( + '/roles-info/:infoId', + wrapAsync(async (req, res) => { + const role = await RolesService.removeRoleInfoById( + req.query.infoId, + req.query.roleId, + req.query.key, + req.currentUser, + ); + + res.status(200).send(role); + }), +); + +/** + * @swagger + * /api/roles/role-info/{roleId}: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Get role information by key + * description: Get specific role information by key + * parameters: + * - in: path + * name: roleId + * description: ID of role to get information for + * required: true + * schema: + * type: string + * - in: query + * name: key + * description: Key of the role information to retrieve + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Role information successfully received + * content: + * application/json: + * schema: + * type: object + * properties: + * info: + * type: string + * description: The role information + * 400: + * description: Invalid ID or key supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Role not found + * 500: + * description: Some server error + */ + +router.get( + '/info-by-key', + wrapAsync(async (req, res) => { + const roleId = req.query.roleId; + const key = req.query.key; + const currentUser = req.currentUser; + let info = await RolesService.getRoleInfoByKey( + key, + roleId, + currentUser, + ); + const role = await RolesDBApi.findBy({ id: roleId }); + if (!role?.role_customization) { + await Promise.all(["pie","bar"].map(async (e)=>{ + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description: `Create some cool ${e} chart`, + modelDefinition: schema.definitions, + }; + const widgetId = await getWidget(payload, currentUser?.id, roleId); + if(widgetId){ + await RolesService.addRoleInfo( + roleId, + currentUser?.id, + 'widgets', + widgetId, + req.currentUser, + ); + } + })) + info = await RolesService.getRoleInfoByKey( + key, + roleId, + currentUser, + ); + } + res.status(200).send(info); + }), +); + +router.post( + '/create_widget', + wrapAsync(async (req, res) => { + const { description, userId, roleId } = req.body; + + const currentUser = req.currentUser; + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description, + modelDefinition: schema.definitions, + }; + + const widgetId = await getWidget(payload, userId, roleId); + + if (widgetId) { + await RolesService.addRoleInfo( + roleId, + userId, + 'widgets', + widgetId, + currentUser, + ); + + return res.status(200).send(widgetId); + } else { + return res.status(400).send(widgetId); + } + }), +); + +/** + * @swagger + * /api/openai/ask: + * post: + * security: + * - bearerAuth: [] + * tags: [OpenAI] + * summary: Ask a question to ChatGPT + * description: Send a question to OpenAI's ChatGPT and get a response + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * question: + * type: string + * description: The question to ask ChatGPT + * apiKey: + * type: string + * description: OpenAI API key + * responses: + * 200: + * description: Question successfully answered + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the request was successful + * data: + * type: string + * description: The answer from ChatGPT + * 400: + * description: Invalid request + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 500: + * description: Some server error + */ +router.post( + '/ask-gpt', + wrapAsync(async (req, res) => { + const { prompt } = req.body; + if (!prompt) { + return res.status(400).send({ + success: false, + error: 'Question and API key are required', + }); + } + + const response = await askGpt(prompt); + + if (response.success) { + return res.status(200).send(response); + } else { + return res.status(500).send(response); + } + }), +); + + +module.exports = router; diff --git a/backend/src/routes/payment_revenues.js b/backend/src/routes/payment_revenues.js new file mode 100644 index 0000000..3485284 --- /dev/null +++ b/backend/src/routes/payment_revenues.js @@ -0,0 +1,421 @@ + +const express = require('express'); + +const Payment_revenuesService = require('../services/payment_revenues'); +const Payment_revenuesDBApi = require('../db/api/payment_revenues'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Payment_revenues: + * type: object + * properties: + + * admin_fee_amount: + * type: integer + * format: int64 + * interest_amount: + * type: integer + * format: int64 + * commission_amount: + * type: integer + * format: int64 + * expense_amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Payment_revenues + * description: The Payment_revenues managing API + */ + +/** +* @swagger +* /api/payment_revenues: +* post: +* security: +* - bearerAuth: [] +* tags: [Payment_revenues] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Payment_revenues" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Payment_revenues" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Payment_revenuesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Payment_revenues" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_revenues" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Payment_revenuesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_revenues/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Payment_revenues" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_revenues" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Payment_revenuesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_revenues/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_revenues" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Payment_revenuesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_revenues/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_revenues" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Payment_revenuesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/payment_revenues: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Get all payment_revenues + * description: Get all payment_revenues + * responses: + * 200: + * description: Payment_revenues list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_revenues" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Payment_revenuesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id', + + 'admin_fee_amount','interest_amount','commission_amount','expense_amount', + 'transaction_date','setlement_datetime', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/payment_revenues/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Count all payment_revenues + * description: Count all payment_revenues + * responses: + * 200: + * description: Payment_revenues count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_revenues" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Payment_revenuesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_revenues/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Find all payment_revenues that match search criteria + * description: Find all payment_revenues that match search criteria + * responses: + * 200: + * description: Payment_revenues list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_revenues" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Payment_revenuesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/payment_revenues/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_revenues] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_revenues" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Payment_revenuesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/payment_status.js b/backend/src/routes/payment_status.js new file mode 100644 index 0000000..d4da39b --- /dev/null +++ b/backend/src/routes/payment_status.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const Payment_statusService = require('../services/payment_status'); +const Payment_statusDBApi = require('../db/api/payment_status'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Payment_status: + * type: object + * properties: + + * name: + * type: string + * default: name + * css: + * type: string + * default: css + + */ + +/** + * @swagger + * tags: + * name: Payment_status + * description: The Payment_status managing API + */ + +/** +* @swagger +* /api/payment_status: +* post: +* security: +* - bearerAuth: [] +* tags: [Payment_status] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Payment_status" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Payment_status" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Payment_statusService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Payment_status" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Payment_statusService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_status/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Payment_status" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Payment_statusService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_status/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Payment_statusService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_status/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Payment_statusService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/payment_status: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Get all payment_status + * description: Get all payment_status + * responses: + * 200: + * description: Payment_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Payment_statusDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name','css', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/payment_status/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Count all payment_status + * description: Count all payment_status + * responses: + * 200: + * description: Payment_status count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Payment_statusDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_status/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Find all payment_status that match search criteria + * description: Find all payment_status that match search criteria + * responses: + * 200: + * description: Payment_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Payment_statusDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/payment_status/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_status] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Payment_statusDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/payment_transactions.js b/backend/src/routes/payment_transactions.js new file mode 100644 index 0000000..23f81a8 --- /dev/null +++ b/backend/src/routes/payment_transactions.js @@ -0,0 +1,425 @@ + +const express = require('express'); + +const Payment_transactionsService = require('../services/payment_transactions'); +const Payment_transactionsDBApi = require('../db/api/payment_transactions'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Payment_transactions: + * type: object + * properties: + + * transaction_amount: + * type: integer + * format: int64 + * deduct_amount: + * type: integer + * format: int64 + * interest_amount: + * type: integer + * format: int64 + * commission_amount: + * type: integer + * format: int64 + * revenue_amount: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Payment_transactions + * description: The Payment_transactions managing API + */ + +/** +* @swagger +* /api/payment_transactions: +* post: +* security: +* - bearerAuth: [] +* tags: [Payment_transactions] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Payment_transactions" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Payment_transactions" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Payment_transactionsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Payment_transactions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Payment_transactionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_transactions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Payment_transactions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_transactions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Payment_transactionsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_transactions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_transactions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Payment_transactionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_transactions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Payment_transactionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/payment_transactions: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Get all payment_transactions + * description: Get all payment_transactions + * responses: + * 200: + * description: Payment_transactions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Payment_transactionsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id', + + 'transaction_amount','deduct_amount','interest_amount','commission_amount','revenue_amount', + 'transaction_datetime','setlement_datetime', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/payment_transactions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Count all payment_transactions + * description: Count all payment_transactions + * responses: + * 200: + * description: Payment_transactions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Payment_transactionsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payment_transactions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Find all payment_transactions that match search criteria + * description: Find all payment_transactions that match search criteria + * responses: + * 200: + * description: Payment_transactions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payment_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Payment_transactionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/payment_transactions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Payment_transactions] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payment_transactions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Payment_transactionsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/payments.js b/backend/src/routes/payments.js new file mode 100644 index 0000000..ecf263e --- /dev/null +++ b/backend/src/routes/payments.js @@ -0,0 +1,447 @@ + +const express = require('express'); + +const PaymentsService = require('../services/payments'); +const PaymentsDBApi = require('../db/api/payments'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Payments: + * type: object + * properties: + + * remark: + * type: string + * default: remark + + * sort: + * type: integer + * format: int64 + * interval: + * type: integer + * format: int64 + + * deduct_amount: + * type: integer + * format: int64 + * deduct_paid_amount: + * type: integer + * format: int64 + * interest_amount: + * type: integer + * format: int64 + * commission_amount: + * type: integer + * format: int64 + * total_amount: + * type: integer + * format: int64 + * total_paid_amount: + * type: integer + * format: int64 + * penalty_amount: + * type: integer + * format: int64 + * pending_amount: + * type: integer + * format: int64 + * cross_amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Payments + * description: The Payments managing API + */ + +/** +* @swagger +* /api/payments: +* post: +* security: +* - bearerAuth: [] +* tags: [Payments] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Payments" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Payments" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PaymentsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Payments" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PaymentsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payments/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Payments" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payments" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await PaymentsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payments/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payments" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await PaymentsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payments/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await PaymentsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/payments: + * get: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Get all payments + * description: Get all payments + * responses: + * 200: + * description: Payments list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await PaymentsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','remark', + 'sort','interval', + 'deduct_amount','deduct_paid_amount','interest_amount','commission_amount','total_amount','total_paid_amount','penalty_amount','pending_amount','cross_amount', + 'start_payment_date','payment_date','last_payment_paid_date', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/payments/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Count all payments + * description: Count all payments + * responses: + * 200: + * description: Payments count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await PaymentsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/payments/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Find all payments that match search criteria + * description: Find all payments that match search criteria + * responses: + * 200: + * description: Payments list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Payments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await PaymentsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/payments/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Payments] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Payments" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await PaymentsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/pexels.js b/backend/src/routes/pexels.js new file mode 100644 index 0000000..8298595 --- /dev/null +++ b/backend/src/routes/pexels.js @@ -0,0 +1,104 @@ +const express = require('express'); +const router = express.Router(); +const { pexelsKey, pexelsQuery } = require('../config'); +const fetch = require('node-fetch'); + +const KEY = pexelsKey; + +router.get('/image', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + const query = pexelsQuery || 'nature'; + const orientation = 'portrait'; + const perPage = 1; + const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + + try { + const response = await fetch(url, { headers }); + const data = await response.json(); + res.status(200).json(data.photos[0]); + } catch (error) { + res.status(200).json({ error: 'Failed to fetch image' }); + } +}); + +router.get('/video', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + const query = pexelsQuery || 'nature'; + const orientation = 'portrait'; + const perPage = 1; + const url = `https://api.pexels.com/videos/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + + try { + const response = await fetch(url, { headers }); + const data = await response.json(); + res.status(200).json(data.videos[0]); + } catch (error) { + res.status(200).json({ error: 'Failed to fetch video' }); + } +}); + +router.get('/multiple-images', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + + const queries = req.query.queries + ? req.query.queries.split(',') + : ['home', 'apple', 'pizza', 'mountains', 'cat']; + const orientation = 'square'; + const perPage = 1; + + const fallbackImage = { + src: 'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg', + photographer: 'Yan Krukau', + photographer_url: 'https://www.pexels.com/@yankrukov', + }; + const fetchFallbackImage = async () => { + try { + const response = await fetch('https://picsum.photos/600'); + return { + src: response.url, + photographer: 'Random Picsum', + photographer_url: 'https://picsum.photos/', + }; + } catch (error) { + return fallbackImage; + } + }; + const fetchImage = async (query) => { + const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + const response = await fetch(url, { headers }); + const data = await response.json(); + return data.photos[0] || null; + }; + + const imagePromises = queries.map((query) => fetchImage(query)); + const imagesResults = await Promise.allSettled(imagePromises); + + const formattedImages = await Promise.all(imagesResults.map(async (result) => { + if (result.status === 'fulfilled' && result.value) { + const image = result.value; + return { + src: image.src?.original || fallbackImage.src, + photographer: image.photographer || fallbackImage.photographer, + photographer_url: image.photographer_url || fallbackImage.photographer_url, + }; + } else { + const fallback = await fetchFallbackImage(); + return { + src: fallback.src || '', + photographer: fallback.photographer || 'Unknown', + photographer_url: fallback.photographer_url || '', + }; + } + })); + + + res.json(formattedImages); +}); + +module.exports = router; diff --git a/backend/src/routes/provinces.js b/backend/src/routes/provinces.js new file mode 100644 index 0000000..8a68e26 --- /dev/null +++ b/backend/src/routes/provinces.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const ProvincesService = require('../services/provinces'); +const ProvincesDBApi = require('../db/api/provinces'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Provinces: + * type: object + * properties: + + * name_kh: + * type: string + * default: name_kh + * name_en: + * type: string + * default: name_en + + */ + +/** + * @swagger + * tags: + * name: Provinces + * description: The Provinces managing API + */ + +/** +* @swagger +* /api/provinces: +* post: +* security: +* - bearerAuth: [] +* tags: [Provinces] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Provinces" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Provinces" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ProvincesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Provinces" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Provinces" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ProvincesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/provinces/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Provinces" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Provinces" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await ProvincesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/provinces/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Provinces" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await ProvincesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/provinces/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Provinces" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await ProvincesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/provinces: + * get: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Get all provinces + * description: Get all provinces + * responses: + * 200: + * description: Provinces list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Provinces" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await ProvincesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_kh','name_en', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/provinces/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Count all provinces + * description: Count all provinces + * responses: + * 200: + * description: Provinces count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Provinces" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await ProvincesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/provinces/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Find all provinces that match search criteria + * description: Find all provinces that match search criteria + * responses: + * 200: + * description: Provinces list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Provinces" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ProvincesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/provinces/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Provinces] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Provinces" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await ProvincesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js new file mode 100644 index 0000000..bbc84fe --- /dev/null +++ b/backend/src/routes/search.js @@ -0,0 +1,47 @@ +const express = require('express'); +const SearchService = require('../services/search'); + +const router = express.Router(); + +/** + * @swagger + * path: + * /api/search: + * post: + * summary: Search + * description: Search results across multiple tables + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * searchQuery: + * type: string + * required: + * - searchQuery + * responses: + * 200: + * description: Successful request + * 400: + * description: Invalid request + * 500: + * description: Internal server error + */ + +router.post('/', async (req, res) => { + const { searchQuery } = req.body; + if (!searchQuery) { + return res.status(400).json({ error: 'Please enter a search query' }); + } + + try { + const foundMatches = await SearchService.search(searchQuery, req.currentUser); + res.json(foundMatches); + } catch (error) { + console.error('Internal Server Error', error); + res.status(500).json({ error: 'Internal Server Error' }); + } + }); + +module.exports = router; diff --git a/backend/src/routes/sexes.js b/backend/src/routes/sexes.js new file mode 100644 index 0000000..92f6666 --- /dev/null +++ b/backend/src/routes/sexes.js @@ -0,0 +1,410 @@ + +const express = require('express'); + +const SexesService = require('../services/sexes'); +const SexesDBApi = require('../db/api/sexes'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Sexes: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Sexes + * description: The Sexes managing API + */ + +/** +* @swagger +* /api/sexes: +* post: +* security: +* - bearerAuth: [] +* tags: [Sexes] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Sexes" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Sexes" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await SexesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Sexes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Sexes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await SexesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/sexes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Sexes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Sexes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await SexesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/sexes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Sexes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await SexesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/sexes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Sexes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await SexesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/sexes: + * get: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Get all sexes + * description: Get all sexes + * responses: + * 200: + * description: Sexes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Sexes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await SexesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/sexes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Count all sexes + * description: Count all sexes + * responses: + * 200: + * description: Sexes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Sexes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await SexesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/sexes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Find all sexes that match search criteria + * description: Find all sexes that match search criteria + * responses: + * 200: + * description: Sexes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Sexes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await SexesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/sexes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Sexes] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Sexes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await SexesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/shareholders.js b/backend/src/routes/shareholders.js new file mode 100644 index 0000000..eee0046 --- /dev/null +++ b/backend/src/routes/shareholders.js @@ -0,0 +1,438 @@ + +const express = require('express'); + +const ShareholdersService = require('../services/shareholders'); +const ShareholdersDBApi = require('../db/api/shareholders'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Shareholders: + * type: object + * properties: + + * name_en: + * type: string + * default: name_en + * name_kh: + * type: string + * default: name_kh + * phone_number: + * type: string + * default: phone_number + * born_place: + * type: string + * default: born_place + * document_type: + * type: string + * default: document_type + * document_number: + * type: string + * default: document_number + * emergency_number: + * type: string + * default: emergency_number + * current_place: + * type: string + * default: current_place + + * earn_rate: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Shareholders + * description: The Shareholders managing API + */ + +/** +* @swagger +* /api/shareholders: +* post: +* security: +* - bearerAuth: [] +* tags: [Shareholders] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Shareholders" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Shareholders" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ShareholdersService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Shareholders" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Shareholders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ShareholdersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/shareholders/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Shareholders" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Shareholders" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await ShareholdersService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/shareholders/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Shareholders" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await ShareholdersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/shareholders/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Shareholders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await ShareholdersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/shareholders: + * get: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Get all shareholders + * description: Get all shareholders + * responses: + * 200: + * description: Shareholders list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Shareholders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await ShareholdersDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_en','name_kh','phone_number','born_place','document_type','document_number','emergency_number','current_place', + + 'earn_rate', + 'date_of_birth','start_work_date', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/shareholders/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Count all shareholders + * description: Count all shareholders + * responses: + * 200: + * description: Shareholders count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Shareholders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await ShareholdersDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/shareholders/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Find all shareholders that match search criteria + * description: Find all shareholders that match search criteria + * responses: + * 200: + * description: Shareholders list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Shareholders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ShareholdersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/shareholders/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Shareholders] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Shareholders" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await ShareholdersDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/staff_status.js b/backend/src/routes/staff_status.js new file mode 100644 index 0000000..e514fc6 --- /dev/null +++ b/backend/src/routes/staff_status.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const Staff_statusService = require('../services/staff_status'); +const Staff_statusDBApi = require('../db/api/staff_status'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Staff_status: + * type: object + * properties: + + * name: + * type: string + * default: name + * css: + * type: string + * default: css + + */ + +/** + * @swagger + * tags: + * name: Staff_status + * description: The Staff_status managing API + */ + +/** +* @swagger +* /api/staff_status: +* post: +* security: +* - bearerAuth: [] +* tags: [Staff_status] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Staff_status" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Staff_status" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Staff_statusService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Staff_status" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staff_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Staff_statusService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staff_status/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Staff_status" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staff_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Staff_statusService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staff_status/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staff_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Staff_statusService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staff_status/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staff_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Staff_statusService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/staff_status: + * get: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Get all staff_status + * description: Get all staff_status + * responses: + * 200: + * description: Staff_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Staff_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Staff_statusDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name','css', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/staff_status/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Count all staff_status + * description: Count all staff_status + * responses: + * 200: + * description: Staff_status count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Staff_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Staff_statusDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staff_status/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Find all staff_status that match search criteria + * description: Find all staff_status that match search criteria + * responses: + * 200: + * description: Staff_status list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Staff_status" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Staff_statusDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/staff_status/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Staff_status] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staff_status" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Staff_statusDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/staffs.js b/backend/src/routes/staffs.js new file mode 100644 index 0000000..6f77ba4 --- /dev/null +++ b/backend/src/routes/staffs.js @@ -0,0 +1,433 @@ + +const express = require('express'); + +const StaffsService = require('../services/staffs'); +const StaffsDBApi = require('../db/api/staffs'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Staffs: + * type: object + * properties: + + * name_en: + * type: string + * default: name_en + * name_kh: + * type: string + * default: name_kh + * phone_number: + * type: string + * default: phone_number + * born_place: + * type: string + * default: born_place + * document_type: + * type: string + * default: document_type + * document_number: + * type: string + * default: document_number + * emergency_number: + * type: string + * default: emergency_number + * current_place: + * type: string + * default: current_place + + * + */ + +/** + * @swagger + * tags: + * name: Staffs + * description: The Staffs managing API + */ + +/** +* @swagger +* /api/staffs: +* post: +* security: +* - bearerAuth: [] +* tags: [Staffs] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Staffs" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Staffs" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await StaffsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Staffs" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staffs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await StaffsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staffs/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Staffs" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staffs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await StaffsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staffs/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staffs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await StaffsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staffs/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staffs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await StaffsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/staffs: + * get: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Get all staffs + * description: Get all staffs + * responses: + * 200: + * description: Staffs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Staffs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await StaffsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_en','name_kh','phone_number','born_place','document_type','document_number','emergency_number','current_place', + + 'date_of_birth','start_work_date', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/staffs/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Count all staffs + * description: Count all staffs + * responses: + * 200: + * description: Staffs count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Staffs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await StaffsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/staffs/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Find all staffs that match search criteria + * description: Find all staffs that match search criteria + * responses: + * 200: + * description: Staffs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Staffs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await StaffsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/staffs/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Staffs] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Staffs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await StaffsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/urls.js b/backend/src/routes/urls.js new file mode 100644 index 0000000..2ee4688 --- /dev/null +++ b/backend/src/routes/urls.js @@ -0,0 +1,414 @@ + +const express = require('express'); + +const UrlsService = require('../services/urls'); +const UrlsDBApi = require('../db/api/urls'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Urls: + * type: object + * properties: + + * uri: + * type: string + * default: uri + * route_name: + * type: string + * default: route_name + + * + */ + +/** + * @swagger + * tags: + * name: Urls + * description: The Urls managing API + */ + +/** +* @swagger +* /api/urls: +* post: +* security: +* - bearerAuth: [] +* tags: [Urls] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Urls" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Urls" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UrlsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Urls" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UrlsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/urls/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Urls" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Urls" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await UrlsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/urls/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Urls" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await UrlsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/urls/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await UrlsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/urls: + * get: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Get all urls + * description: Get all urls + * responses: + * 200: + * description: Urls list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await UrlsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','uri','route_name', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/urls/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Count all urls + * description: Count all urls + * responses: + * 200: + * description: Urls count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await UrlsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/urls/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Find all urls that match search criteria + * description: Find all urls that match search criteria + * responses: + * 200: + * description: Urls list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await UrlsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/urls/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Urls] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Urls" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await UrlsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/user_has_menu.js b/backend/src/routes/user_has_menu.js new file mode 100644 index 0000000..7ac99aa --- /dev/null +++ b/backend/src/routes/user_has_menu.js @@ -0,0 +1,410 @@ + +const express = require('express'); + +const User_has_menuService = require('../services/user_has_menu'); +const User_has_menuDBApi = require('../db/api/user_has_menu'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * User_has_menu: + * type: object + * properties: + + * status: + * type: string + * default: status + + */ + +/** + * @swagger + * tags: + * name: User_has_menu + * description: The User_has_menu managing API + */ + +/** +* @swagger +* /api/user_has_menu: +* post: +* security: +* - bearerAuth: [] +* tags: [User_has_menu] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/User_has_menu" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/User_has_menu" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await User_has_menuService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/User_has_menu" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_has_menu" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await User_has_menuService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_has_menu/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/User_has_menu" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_has_menu" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await User_has_menuService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_has_menu/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_has_menu" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await User_has_menuService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_has_menu/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_has_menu" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await User_has_menuService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/user_has_menu: + * get: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Get all user_has_menu + * description: Get all user_has_menu + * responses: + * 200: + * description: User_has_menu list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_has_menu" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await User_has_menuDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','status', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/user_has_menu/count: + * get: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Count all user_has_menu + * description: Count all user_has_menu + * responses: + * 200: + * description: User_has_menu count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_has_menu" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await User_has_menuDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_has_menu/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Find all user_has_menu that match search criteria + * description: Find all user_has_menu that match search criteria + * responses: + * 200: + * description: User_has_menu list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_has_menu" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await User_has_menuDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/user_has_menu/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [User_has_menu] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_has_menu" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await User_has_menuDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/user_type_urls.js b/backend/src/routes/user_type_urls.js new file mode 100644 index 0000000..205d755 --- /dev/null +++ b/backend/src/routes/user_type_urls.js @@ -0,0 +1,406 @@ + +const express = require('express'); + +const User_type_urlsService = require('../services/user_type_urls'); +const User_type_urlsDBApi = require('../db/api/user_type_urls'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * User_type_urls: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: User_type_urls + * description: The User_type_urls managing API + */ + +/** +* @swagger +* /api/user_type_urls: +* post: +* security: +* - bearerAuth: [] +* tags: [User_type_urls] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/User_type_urls" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/User_type_urls" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await User_type_urlsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/User_type_urls" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_type_urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await User_type_urlsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_type_urls/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/User_type_urls" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_type_urls" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await User_type_urlsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_type_urls/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_type_urls" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await User_type_urlsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_type_urls/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_type_urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await User_type_urlsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/user_type_urls: + * get: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Get all user_type_urls + * description: Get all user_type_urls + * responses: + * 200: + * description: User_type_urls list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_type_urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await User_type_urlsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/user_type_urls/count: + * get: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Count all user_type_urls + * description: Count all user_type_urls + * responses: + * 200: + * description: User_type_urls count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_type_urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await User_type_urlsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_type_urls/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Find all user_type_urls that match search criteria + * description: Find all user_type_urls that match search criteria + * responses: + * 200: + * description: User_type_urls list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_type_urls" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await User_type_urlsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/user_type_urls/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [User_type_urls] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_type_urls" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await User_type_urlsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/user_types.js b/backend/src/routes/user_types.js new file mode 100644 index 0000000..3d8d83a --- /dev/null +++ b/backend/src/routes/user_types.js @@ -0,0 +1,410 @@ + +const express = require('express'); + +const User_typesService = require('../services/user_types'); +const User_typesDBApi = require('../db/api/user_types'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * User_types: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: User_types + * description: The User_types managing API + */ + +/** +* @swagger +* /api/user_types: +* post: +* security: +* - bearerAuth: [] +* tags: [User_types] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/User_types" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/User_types" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await User_typesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/User_types" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await User_typesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_types/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/User_types" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await User_typesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_types/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await User_typesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_types/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await User_typesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/user_types: + * get: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Get all user_types + * description: Get all user_types + * responses: + * 200: + * description: User_types list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await User_typesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/user_types/count: + * get: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Count all user_types + * description: Count all user_types + * responses: + * 200: + * description: User_types count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await User_typesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/user_types/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Find all user_types that match search criteria + * description: Find all user_types that match search criteria + * responses: + * 200: + * description: User_types list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/User_types" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await User_typesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/user_types/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [User_types] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/User_types" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await User_typesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js new file mode 100644 index 0000000..441441b --- /dev/null +++ b/backend/src/routes/users.js @@ -0,0 +1,421 @@ + +const express = require('express'); + +const UsersService = require('../services/users'); +const UsersDBApi = require('../db/api/users'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Users: + * type: object + * properties: + + * firstName: + * type: string + * default: firstName + * lastName: + * type: string + * default: lastName + * phoneNumber: + * type: string + * default: phoneNumber + * email: + * type: string + * default: email + + */ + +/** + * @swagger + * tags: + * name: Users + * description: The Users managing API + */ + +/** +* @swagger +* /api/users: +* post: +* security: +* - bearerAuth: [] +* tags: [Users] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Users" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Users" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UsersService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Users" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UsersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/users/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Users" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await UsersService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/users/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await UsersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/users/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await UsersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/users: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get all users + * description: Get all users + * responses: + * 200: + * description: Users list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await UsersDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','firstName','lastName','phoneNumber','email', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/users/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Count all users + * description: Count all users + * responses: + * 200: + * description: Users count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await UsersDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/users/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Find all users that match search criteria + * description: Find all users that match search criteria + * responses: + * 200: + * description: Users list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await UsersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/users/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await UsersDBApi.findBy( + { id: req.params.id }, + ); + + delete payload.password; + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/villages.js b/backend/src/routes/villages.js new file mode 100644 index 0000000..dad7e8b --- /dev/null +++ b/backend/src/routes/villages.js @@ -0,0 +1,413 @@ + +const express = require('express'); + +const VillagesService = require('../services/villages'); +const VillagesDBApi = require('../db/api/villages'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Villages: + * type: object + * properties: + + * name_kh: + * type: string + * default: name_kh + * name_en: + * type: string + * default: name_en + + */ + +/** + * @swagger + * tags: + * name: Villages + * description: The Villages managing API + */ + +/** +* @swagger +* /api/villages: +* post: +* security: +* - bearerAuth: [] +* tags: [Villages] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Villages" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Villages" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await VillagesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Villages" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Villages" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await VillagesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/villages/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Villages" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Villages" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await VillagesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/villages/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Villages" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await VillagesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/villages/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Villages" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await VillagesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/villages: + * get: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Get all villages + * description: Get all villages + * responses: + * 200: + * description: Villages list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Villages" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await VillagesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name_kh','name_en', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/villages/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Count all villages + * description: Count all villages + * responses: + * 200: + * description: Villages count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Villages" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await VillagesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/villages/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Find all villages that match search criteria + * description: Find all villages that match search criteria + * responses: + * 200: + * description: Villages list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Villages" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await VillagesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/villages/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Villages] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Villages" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await VillagesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js new file mode 100644 index 0000000..014feae --- /dev/null +++ b/backend/src/services/auth.js @@ -0,0 +1,257 @@ +const UsersDBApi = require('../db/api/users'); +const ValidationError = require('./notifications/errors/validation'); +const ForbiddenError = require('./notifications/errors/forbidden'); +const bcrypt = require('bcrypt'); +const config = require('../config'); +const helpers = require('../helpers'); + +class Auth { + static async signup(email, password, options = {}, host) { + const user = await UsersDBApi.findBy({email}); + + const hashedPassword = await bcrypt.hash( + password, + config.bcrypt.saltRounds, + ); + + if (user) { + if (user.authenticationUid) { + throw new ValidationError( + 'auth.emailAlreadyInUse', + ); + } + + if (user.disabled) { + throw new ValidationError( + 'auth.userDisabled', + ); + } + + await UsersDBApi.updatePassword( + user.id, + hashedPassword, + options, + ); + + // Send Email Address Verification Email + + const data = { + user: { + id: user.id, + email: user.email + } + }; + + return helpers.jwtSign(data); + } + + const newUser = await UsersDBApi.createFromAuth( + { + firstName: email.split('@')[0], + password: hashedPassword, + email: email, + }, + options, + ); + + // Send Email Address Verification Email + + const data = { + user: { + id: newUser.id, + email: newUser.email + } + }; + + return helpers.jwtSign(data); + } + + static async signin(email, password, options = {}) { + const user = await UsersDBApi.findBy({email}); + + if (!user) { + throw new ValidationError( + 'auth.userNotFound', + ); + } + + if (user.disabled) { + throw new ValidationError( + 'auth.userDisabled', + ); + } + + if (!user.password) { + throw new ValidationError( + 'auth.wrongPassword', + ); + } + + // Set emailVerified param + + if (!user.emailVerified) { + throw new ValidationError( + 'auth.userNotVerified', + ); + } + + const passwordsMatch = await bcrypt.compare( + password, + user.password, + ); + + if (!passwordsMatch) { + throw new ValidationError( + 'auth.wrongPassword', + ); + } + + const data = { + user: { + id: user.id, + email: user.email + } + }; + + return helpers.jwtSign(data); + } + + // sendEmailAddressVerificationEmail + + static async sendPasswordResetEmail(email, type = 'register', host) { + let link; + + try { + const token = await UsersDBApi.generatePasswordResetToken( + email, + ); + link = `${host}/password-reset?token=${token}`; + } catch (error) { + console.error(error); + throw new ValidationError( + 'auth.passwordReset.error', + ); + } + + let passwordResetEmail; + if (type === 'register') { + + } + + // password Reset Email + return true; + } + + static async verifyEmail(token, options = {}) { + const user = await UsersDBApi.findByEmailVerificationToken( + token, + options, + ); + + if (!user) { + throw new ValidationError( + 'auth.emailAddressVerificationEmail.invalidToken', + ); + } + + return UsersDBApi.markEmailVerified( + user.id, + options, + ); + } + + static async passwordUpdate(currentPassword, newPassword, options) { + const currentUser = options.currentUser || null; + if (!currentUser) { + throw new ForbiddenError(); + } + + const currentPasswordMatch = await bcrypt.compare( + currentPassword, + currentUser.password, + ); + + if (!currentPasswordMatch) { + throw new ValidationError( + 'auth.wrongPassword' + ) + } + + const newPasswordMatch = await bcrypt.compare( + newPassword, + currentUser.password, + ); + + if (newPasswordMatch) { + throw new ValidationError( + 'auth.passwordUpdate.samePassword' + ) + } + + const hashedPassword = await bcrypt.hash( + newPassword, + config.bcrypt.saltRounds, + ); + + return UsersDBApi.updatePassword( + currentUser.id, + hashedPassword, + options, + ); + } + + static async passwordReset( + token, + password, + options = {}, + ) { + const user = await UsersDBApi.findByPasswordResetToken( + token, + options, + ); + + if (!user) { + throw new ValidationError( + 'auth.passwordReset.invalidToken', + ); + } + + const hashedPassword = await bcrypt.hash( + password, + config.bcrypt.saltRounds, + ); + + return UsersDBApi.updatePassword( + user.id, + hashedPassword, + options, + ); + } + + static async updateProfile(data, currentUser) { + let transaction = await db.sequelize.transaction(); + + try { + await UsersDBApi.findBy( + {id: currentUser.id}, + {transaction}, + ); + + await UsersDBApi.update( + currentUser.id, + data, + { + currentUser, + transaction + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +} + +module.exports = Auth; diff --git a/backend/src/services/branches.js b/backend/src/services/branches.js new file mode 100644 index 0000000..589ffba --- /dev/null +++ b/backend/src/services/branches.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const BranchesDBApi = require('../db/api/branches'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class BranchesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await BranchesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await BranchesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let branches = await BranchesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!branches) { + throw new ValidationError( + 'branchesNotFound', + ); + } + + const updatedBranches = await BranchesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedBranches; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await BranchesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await BranchesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/calendars.js b/backend/src/services/calendars.js new file mode 100644 index 0000000..051c79d --- /dev/null +++ b/backend/src/services/calendars.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const CalendarsDBApi = require('../db/api/calendars'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class CalendarsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await CalendarsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await CalendarsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let calendars = await CalendarsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!calendars) { + throw new ValidationError( + 'calendarsNotFound', + ); + } + + const updatedCalendars = await CalendarsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedCalendars; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await CalendarsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await CalendarsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/client_status.js b/backend/src/services/client_status.js new file mode 100644 index 0000000..92849a1 --- /dev/null +++ b/backend/src/services/client_status.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Client_statusDBApi = require('../db/api/client_status'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Client_statusService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Client_statusDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Client_statusDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let client_status = await Client_statusDBApi.findBy( + {id}, + {transaction}, + ); + + if (!client_status) { + throw new ValidationError( + 'client_statusNotFound', + ); + } + + const updatedClient_status = await Client_statusDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedClient_status; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Client_statusDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Client_statusDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/clients.js b/backend/src/services/clients.js new file mode 100644 index 0000000..f27e277 --- /dev/null +++ b/backend/src/services/clients.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const ClientsDBApi = require('../db/api/clients'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class ClientsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ClientsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await ClientsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let clients = await ClientsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!clients) { + throw new ValidationError( + 'clientsNotFound', + ); + } + + const updatedClients = await ClientsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedClients; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ClientsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ClientsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/communes.js b/backend/src/services/communes.js new file mode 100644 index 0000000..e522434 --- /dev/null +++ b/backend/src/services/communes.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const CommunesDBApi = require('../db/api/communes'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class CommunesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await CommunesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await CommunesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let communes = await CommunesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!communes) { + throw new ValidationError( + 'communesNotFound', + ); + } + + const updatedCommunes = await CommunesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedCommunes; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await CommunesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await CommunesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/deposits.js b/backend/src/services/deposits.js new file mode 100644 index 0000000..5c851f0 --- /dev/null +++ b/backend/src/services/deposits.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const DepositsDBApi = require('../db/api/deposits'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class DepositsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await DepositsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await DepositsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let deposits = await DepositsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!deposits) { + throw new ValidationError( + 'depositsNotFound', + ); + } + + const updatedDeposits = await DepositsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedDeposits; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await DepositsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await DepositsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/districts.js b/backend/src/services/districts.js new file mode 100644 index 0000000..065a686 --- /dev/null +++ b/backend/src/services/districts.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const DistrictsDBApi = require('../db/api/districts'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class DistrictsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await DistrictsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await DistrictsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let districts = await DistrictsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!districts) { + throw new ValidationError( + 'districtsNotFound', + ); + } + + const updatedDistricts = await DistrictsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedDistricts; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await DistrictsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await DistrictsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/document_type.js b/backend/src/services/document_type.js new file mode 100644 index 0000000..b9f3ba3 --- /dev/null +++ b/backend/src/services/document_type.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Document_typeDBApi = require('../db/api/document_type'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Document_typeService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Document_typeDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Document_typeDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let document_type = await Document_typeDBApi.findBy( + {id}, + {transaction}, + ); + + if (!document_type) { + throw new ValidationError( + 'document_typeNotFound', + ); + } + + const updatedDocument_type = await Document_typeDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedDocument_type; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Document_typeDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Document_typeDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/expense_items.js b/backend/src/services/expense_items.js new file mode 100644 index 0000000..606e3ff --- /dev/null +++ b/backend/src/services/expense_items.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Expense_itemsDBApi = require('../db/api/expense_items'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Expense_itemsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Expense_itemsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Expense_itemsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let expense_items = await Expense_itemsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!expense_items) { + throw new ValidationError( + 'expense_itemsNotFound', + ); + } + + const updatedExpense_items = await Expense_itemsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedExpense_items; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Expense_itemsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Expense_itemsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/expense_types.js b/backend/src/services/expense_types.js new file mode 100644 index 0000000..02e9f19 --- /dev/null +++ b/backend/src/services/expense_types.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Expense_typesDBApi = require('../db/api/expense_types'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Expense_typesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Expense_typesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Expense_typesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let expense_types = await Expense_typesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!expense_types) { + throw new ValidationError( + 'expense_typesNotFound', + ); + } + + const updatedExpense_types = await Expense_typesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedExpense_types; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Expense_typesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Expense_typesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/group_menus.js b/backend/src/services/group_menus.js new file mode 100644 index 0000000..255729f --- /dev/null +++ b/backend/src/services/group_menus.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Group_menusDBApi = require('../db/api/group_menus'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Group_menusService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Group_menusDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Group_menusDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let group_menus = await Group_menusDBApi.findBy( + {id}, + {transaction}, + ); + + if (!group_menus) { + throw new ValidationError( + 'group_menusNotFound', + ); + } + + const updatedGroup_menus = await Group_menusDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedGroup_menus; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Group_menusDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Group_menusDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/guarantor.js b/backend/src/services/guarantor.js new file mode 100644 index 0000000..b10c25a --- /dev/null +++ b/backend/src/services/guarantor.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const GuarantorDBApi = require('../db/api/guarantor'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class GuarantorService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await GuarantorDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await GuarantorDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let guarantor = await GuarantorDBApi.findBy( + {id}, + {transaction}, + ); + + if (!guarantor) { + throw new ValidationError( + 'guarantorNotFound', + ); + } + + const updatedGuarantor = await GuarantorDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedGuarantor; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await GuarantorDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await GuarantorDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/interest_rates.js b/backend/src/services/interest_rates.js new file mode 100644 index 0000000..fa79a65 --- /dev/null +++ b/backend/src/services/interest_rates.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Interest_ratesDBApi = require('../db/api/interest_rates'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Interest_ratesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Interest_ratesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Interest_ratesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let interest_rates = await Interest_ratesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!interest_rates) { + throw new ValidationError( + 'interest_ratesNotFound', + ); + } + + const updatedInterest_rates = await Interest_ratesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedInterest_rates; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Interest_ratesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Interest_ratesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/loan_status.js b/backend/src/services/loan_status.js new file mode 100644 index 0000000..05e4b7d --- /dev/null +++ b/backend/src/services/loan_status.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Loan_statusDBApi = require('../db/api/loan_status'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Loan_statusService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Loan_statusDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Loan_statusDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let loan_status = await Loan_statusDBApi.findBy( + {id}, + {transaction}, + ); + + if (!loan_status) { + throw new ValidationError( + 'loan_statusNotFound', + ); + } + + const updatedLoan_status = await Loan_statusDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedLoan_status; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Loan_statusDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Loan_statusDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/loan_types.js b/backend/src/services/loan_types.js new file mode 100644 index 0000000..a493b05 --- /dev/null +++ b/backend/src/services/loan_types.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Loan_typesDBApi = require('../db/api/loan_types'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Loan_typesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Loan_typesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Loan_typesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let loan_types = await Loan_typesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!loan_types) { + throw new ValidationError( + 'loan_typesNotFound', + ); + } + + const updatedLoan_types = await Loan_typesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedLoan_types; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Loan_typesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Loan_typesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/loans.js b/backend/src/services/loans.js new file mode 100644 index 0000000..1bd9188 --- /dev/null +++ b/backend/src/services/loans.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const LoansDBApi = require('../db/api/loans'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class LoansService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LoansDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await LoansDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let loans = await LoansDBApi.findBy( + {id}, + {transaction}, + ); + + if (!loans) { + throw new ValidationError( + 'loansNotFound', + ); + } + + const updatedLoans = await LoansDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedLoans; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LoansDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LoansDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/members.js b/backend/src/services/members.js new file mode 100644 index 0000000..d7838e2 --- /dev/null +++ b/backend/src/services/members.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const MembersDBApi = require('../db/api/members'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class MembersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await MembersDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await MembersDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let members = await MembersDBApi.findBy( + {id}, + {transaction}, + ); + + if (!members) { + throw new ValidationError( + 'membersNotFound', + ); + } + + const updatedMembers = await MembersDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedMembers; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await MembersDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await MembersDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/menus.js b/backend/src/services/menus.js new file mode 100644 index 0000000..8c49b4c --- /dev/null +++ b/backend/src/services/menus.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const MenusDBApi = require('../db/api/menus'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class MenusService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await MenusDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await MenusDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let menus = await MenusDBApi.findBy( + {id}, + {transaction}, + ); + + if (!menus) { + throw new ValidationError( + 'menusNotFound', + ); + } + + const updatedMenus = await MenusDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedMenus; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await MenusDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await MenusDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/notifications/errors/forbidden.js b/backend/src/services/notifications/errors/forbidden.js new file mode 100644 index 0000000..33e5dc2 --- /dev/null +++ b/backend/src/services/notifications/errors/forbidden.js @@ -0,0 +1,17 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ForbiddenError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = + message || getNotification('errors.forbidden.message'); + + super(message); + this.code = 403; + } +}; diff --git a/backend/src/services/notifications/errors/validation.js b/backend/src/services/notifications/errors/validation.js new file mode 100644 index 0000000..cf3130c --- /dev/null +++ b/backend/src/services/notifications/errors/validation.js @@ -0,0 +1,18 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ValidationError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = + message || + getNotification('errors.validation.message'); + + super(message); + this.code = 400; + } +}; diff --git a/backend/src/services/notifications/helpers.js b/backend/src/services/notifications/helpers.js new file mode 100644 index 0000000..b2f31fd --- /dev/null +++ b/backend/src/services/notifications/helpers.js @@ -0,0 +1,35 @@ +const _get = require('lodash/get'); +const errors = require('./list'); + +function format(message, args) { + if (!message) { + return null; + } + + return message.replace(/{(\d+)}/g, function ( + match, + number, + ) { + return typeof args[number] != 'undefined' + ? args[number] + : match; + }); +} + +const isNotification = (key) => { + const message = _get(errors, key); + return !!message; +}; + +const getNotification = (key, ...args) => { + const message = _get(errors, key); + + if (!message) { + return key; + } + + return format(message, args); +}; + +exports.getNotification = getNotification; +exports.isNotification = isNotification; diff --git a/backend/src/services/notifications/list.js b/backend/src/services/notifications/list.js new file mode 100644 index 0000000..2ad617b --- /dev/null +++ b/backend/src/services/notifications/list.js @@ -0,0 +1,104 @@ +const errors = { + app: { + title: 'meng-leap-cash', + }, + + auth: { + userDisabled: 'Your account is disabled', + forbidden: 'Forbidden', + unauthorized: 'Unauthorized', + userNotFound: `Sorry, we don't recognize your credentials`, + wrongPassword: `Sorry, we don't recognize your credentials`, + weakPassword: 'This password is too weak', + emailAlreadyInUse: 'Email is already in use', + invalidEmail: 'Please provide a valid email', + passwordReset: { + invalidToken: + 'Password reset link is invalid or has expired', + error: `Email not recognized`, + }, + passwordUpdate: { + samePassword: `You can't use the same password. Please create new password` + }, + userNotVerified: `Sorry, your email has not been verified yet`, + emailAddressVerificationEmail: { + invalidToken: + 'Email verification link is invalid or has expired', + error: `Email not recognized`, + }, + }, + + iam: { + errors: { + userAlreadyExists: + 'User with this email already exists', + userNotFound: 'User not found', + disablingHimself: `You can't disable yourself`, + revokingOwnPermission: `You can't revoke your own owner permission`, + deletingHimself: `You can't delete yourself`, + emailRequired: 'Email is required', + }, + }, + + importer: { + errors: { + invalidFileEmpty: 'The file is empty', + invalidFileExcel: + 'Only excel (.xlsx) files are allowed', + invalidFileUpload: + 'Invalid file. Make sure you are using the last version of the template.', + importHashRequired: 'Import hash is required', + importHashExistent: 'Data has already been imported', + userEmailMissing: 'Some items in the CSV do not have an email', + }, + }, + + errors: { + forbidden: { + message: 'Forbidden', + }, + validation: { + message: 'An error occurred', + }, + searchQueryRequired: { + message: 'Search query is required', + }, + }, + + emails: { + invitation: { + subject: `You've been invited to {0}`, + body: ` +

Hello,

+

You've been invited to {0} set password for your {1} account.

+

{2}

+

Thanks,

+

Your {0} team

+ `, + }, + emailAddressVerification: { + subject: `Verify your email for {0}`, + body: ` +

Hello,

+

Follow this link to verify your email address.

+

{0}

+

If you didn't ask to verify this address, you can ignore this email.

+

Thanks,

+

Your {1} team

+ `, + }, + passwordReset: { + subject: `Reset your password for {0}`, + body: ` +

Hello,

+

Follow this link to reset your {0} password for your {1} account.

+

{2}

+

If you didn't ask to reset your password, you can ignore this email.

+

Thanks,

+

Your {0} team

+ `, + }, + }, +}; + +module.exports = errors; diff --git a/backend/src/services/openai.js b/backend/src/services/openai.js new file mode 100644 index 0000000..64ea2ee --- /dev/null +++ b/backend/src/services/openai.js @@ -0,0 +1,51 @@ +const axios = require('axios'); +const { v4: uuid } = require('uuid'); +const RoleService = require('./roles'); +const config = require('../config'); + +module.exports = class OpenAiService { + static async askGpt(prompt) { + if (!config.gpt_key) { + return { + success: false, + error: 'API key is required' + }; + } + try { + const response = await axios.post( + 'https://api.openai.com/v1/chat/completions', + { + model: 'gpt-4o', + messages: [ + { role: 'user', content: prompt } + ] + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.gpt_key}` + } + } + ); + + if (response.status >= 200 && response.status < 300) { + return { + success: true, + data: response.data.choices[0].message.content + }; + } else { + console.error('Error asking question to ChatGPT:', response.data); + return { + success: false, + error: response.data + }; + } + } catch (error) { + console.error('Error asking question to ChatGPT:', error.response?.data || error.message); + return { + success: false, + error: error.response?.data || error.message + }; + } + } +}; diff --git a/backend/src/services/payment_revenues.js b/backend/src/services/payment_revenues.js new file mode 100644 index 0000000..5b8704e --- /dev/null +++ b/backend/src/services/payment_revenues.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Payment_revenuesDBApi = require('../db/api/payment_revenues'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Payment_revenuesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Payment_revenuesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Payment_revenuesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let payment_revenues = await Payment_revenuesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!payment_revenues) { + throw new ValidationError( + 'payment_revenuesNotFound', + ); + } + + const updatedPayment_revenues = await Payment_revenuesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedPayment_revenues; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Payment_revenuesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Payment_revenuesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/payment_status.js b/backend/src/services/payment_status.js new file mode 100644 index 0000000..9681b83 --- /dev/null +++ b/backend/src/services/payment_status.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Payment_statusDBApi = require('../db/api/payment_status'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Payment_statusService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Payment_statusDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Payment_statusDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let payment_status = await Payment_statusDBApi.findBy( + {id}, + {transaction}, + ); + + if (!payment_status) { + throw new ValidationError( + 'payment_statusNotFound', + ); + } + + const updatedPayment_status = await Payment_statusDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedPayment_status; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Payment_statusDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Payment_statusDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/payment_transactions.js b/backend/src/services/payment_transactions.js new file mode 100644 index 0000000..673f226 --- /dev/null +++ b/backend/src/services/payment_transactions.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Payment_transactionsDBApi = require('../db/api/payment_transactions'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Payment_transactionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Payment_transactionsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Payment_transactionsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let payment_transactions = await Payment_transactionsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!payment_transactions) { + throw new ValidationError( + 'payment_transactionsNotFound', + ); + } + + const updatedPayment_transactions = await Payment_transactionsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedPayment_transactions; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Payment_transactionsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Payment_transactionsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/payments.js b/backend/src/services/payments.js new file mode 100644 index 0000000..eef9ab6 --- /dev/null +++ b/backend/src/services/payments.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const PaymentsDBApi = require('../db/api/payments'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class PaymentsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await PaymentsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await PaymentsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let payments = await PaymentsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!payments) { + throw new ValidationError( + 'paymentsNotFound', + ); + } + + const updatedPayments = await PaymentsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedPayments; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PaymentsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PaymentsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/provinces.js b/backend/src/services/provinces.js new file mode 100644 index 0000000..1ec5feb --- /dev/null +++ b/backend/src/services/provinces.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const ProvincesDBApi = require('../db/api/provinces'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class ProvincesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ProvincesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await ProvincesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let provinces = await ProvincesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!provinces) { + throw new ValidationError( + 'provincesNotFound', + ); + } + + const updatedProvinces = await ProvincesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedProvinces; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ProvincesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ProvincesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/search.js b/backend/src/services/search.js new file mode 100644 index 0000000..1e7b333 --- /dev/null +++ b/backend/src/services/search.js @@ -0,0 +1,491 @@ +const db = require('../db/models'); +const ValidationError = require('./notifications/errors/validation'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +/** + * @param {string} permission + * @param {object} currentUser + */ +async function checkPermissions(permission, currentUser) { + + if (!currentUser) { + throw new ValidationError('auth.unauthorized'); + } + + const userPermission = currentUser.custom_permissions.find( + (cp) => cp.name === permission, + ); + + if (userPermission) { + return true; + } + + try { + if (!currentUser.app_role) { + throw new ValidationError('auth.forbidden'); + } + + const permissions = await currentUser.app_role.getPermissions(); + + return !!permissions.find((p) => p.name === permission); + } catch (e) { + throw e; + } +} + +module.exports = class SearchService { + static async search(searchQuery, currentUser) { + try { + if (!searchQuery) { + throw new ValidationError('iam.errors.searchQueryRequired'); + } + const tableColumns = { + + "users": [ + + "firstName", + + "lastName", + + "phoneNumber", + + "email", + + ], + + "branches": [ + + "code", + + "name", + + "description", + + ], + + "calendars": [ + + "description", + + "flag", + + ], + + "client_status": [ + + "name", + + ], + + "clients": [ + + "code", + + "name_en", + + "name_kh", + + "phone_number", + + "document_number", + + ], + + "communes": [ + + "name_kh", + + "name_en", + + ], + + "districts": [ + + "name_kh", + + "name_en", + + ], + + "document_type": [ + + "name", + + ], + + "expense_items": [ + + "description", + + ], + + "expense_types": [ + + "name_kh", + + "name_en", + + ], + + "group_menus": [ + + "name", + + ], + + "guarantor": [ + + "full_name", + + "document_type", + + "document_number", + + "phone_number", + + "full_address_input", + + ], + + "interest_rates": [ + + "code", + + "name", + + "css", + + "setting", + + ], + + "loan_status": [ + + "name", + + "css", + + ], + + "loan_types": [ + + "name_kh", + + "name_en", + + ], + + "loans": [ + + "code", + + "purpose", + + ], + + "members": [ + + "name_kh", + + "name_en", + + "phone_number", + + ], + + "menus": [ + + "label", + + "url", + + "active_url", + + "permission", + + "icon", + + ], + + "payment_status": [ + + "name", + + "css", + + ], + + "payments": [ + + "remark", + + ], + + "provinces": [ + + "name_kh", + + "name_en", + + ], + + "sexes": [ + + "name", + + ], + + "shareholders": [ + + "name_en", + + "name_kh", + + "phone_number", + + "born_place", + + "document_type", + + "document_number", + + "emergency_number", + + "current_place", + + ], + + "staff_status": [ + + "name", + + "css", + + ], + + "staffs": [ + + "name_en", + + "name_kh", + + "phone_number", + + "born_place", + + "document_type", + + "document_number", + + "emergency_number", + + "current_place", + + ], + + "urls": [ + + "uri", + + "route_name", + + ], + + "user_has_menu": [ + + "status", + + ], + + "user_types": [ + + "name", + + ], + + "villages": [ + + "name_kh", + + "name_en", + + ], + + }; + const columnsInt = { + + "deposits": [ + + "deposit_amount", + + ], + + "expense_items": [ + + "expense_amount", + + ], + + "interest_rates": [ + + "rate", + + "commission_rate", + + "interval", + + "sort", + + ], + + "loans": [ + + "principal_amount", + + "term", + + "pending_amount", + + "last_pending_amount", + + "rate", + + "commission_rate", + + "finish_discount", + + "finish_discount_amount", + + "admin_rate", + + "admin_amount", + + ], + + "payment_revenues": [ + + "admin_fee_amount", + + "interest_amount", + + "commission_amount", + + "expense_amount", + + ], + + "payment_transactions": [ + + "transaction_amount", + + "deduct_amount", + + "interest_amount", + + "commission_amount", + + "revenue_amount", + + ], + + "payments": [ + + "sort", + + "deduct_amount", + + "deduct_paid_amount", + + "interval", + + "interest_amount", + + "commission_amount", + + "total_amount", + + "total_paid_amount", + + "penalty_amount", + + "pending_amount", + + "cross_amount", + + ], + + "shareholders": [ + + "earn_rate", + + ], + + }; + + let allFoundRecords = []; + + for (const tableName in tableColumns) { + if (tableColumns.hasOwnProperty(tableName)) { + const attributesToSearch = tableColumns[tableName]; + const attributesIntToSearch = columnsInt[tableName] || []; + const whereCondition = { + [Op.or]: [ + ...attributesToSearch.map(attribute => ({ + [attribute]: { + [Op.iLike] : `%${searchQuery}%`, + }, + })), + ...attributesIntToSearch.map(attribute => ( + Sequelize.where( + Sequelize.cast(Sequelize.col(`${tableName}.${attribute}`), 'varchar'), + { [Op.iLike]: `%${searchQuery}%` } + ) + )), + ], + }; + + const hasPermission = await checkPermissions(`READ_${tableName.toUpperCase()}`, currentUser); + if (!hasPermission) { + continue; + } + + const foundRecords = await db[tableName].findAll({ + where: whereCondition, + attributes: [...tableColumns[tableName], 'id', ...attributesIntToSearch], + }); + + const modifiedRecords = foundRecords.map((record) => { + const matchAttribute = []; + + for (const attribute of attributesToSearch) { + if (record[attribute]?.toLowerCase()?.includes(searchQuery.toLowerCase())) { + matchAttribute.push(attribute); + } + } + + for (const attribute of attributesIntToSearch) { + const castedValue = String(record[attribute]); + if (castedValue && castedValue.toLowerCase().includes(searchQuery.toLowerCase())) { + matchAttribute.push(attribute); + } + } + + return { + ...record.get(), + matchAttribute, + tableName, + }; + }); + + allFoundRecords = allFoundRecords.concat(modifiedRecords); + } + } + + return allFoundRecords; + } catch (error) { + throw error; + } + } +} diff --git a/backend/src/services/sexes.js b/backend/src/services/sexes.js new file mode 100644 index 0000000..4644105 --- /dev/null +++ b/backend/src/services/sexes.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const SexesDBApi = require('../db/api/sexes'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class SexesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await SexesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await SexesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let sexes = await SexesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!sexes) { + throw new ValidationError( + 'sexesNotFound', + ); + } + + const updatedSexes = await SexesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedSexes; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await SexesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await SexesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/shareholders.js b/backend/src/services/shareholders.js new file mode 100644 index 0000000..64eb817 --- /dev/null +++ b/backend/src/services/shareholders.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const ShareholdersDBApi = require('../db/api/shareholders'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class ShareholdersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ShareholdersDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await ShareholdersDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let shareholders = await ShareholdersDBApi.findBy( + {id}, + {transaction}, + ); + + if (!shareholders) { + throw new ValidationError( + 'shareholdersNotFound', + ); + } + + const updatedShareholders = await ShareholdersDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedShareholders; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ShareholdersDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ShareholdersDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/staff_status.js b/backend/src/services/staff_status.js new file mode 100644 index 0000000..281110f --- /dev/null +++ b/backend/src/services/staff_status.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Staff_statusDBApi = require('../db/api/staff_status'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Staff_statusService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Staff_statusDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Staff_statusDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let staff_status = await Staff_statusDBApi.findBy( + {id}, + {transaction}, + ); + + if (!staff_status) { + throw new ValidationError( + 'staff_statusNotFound', + ); + } + + const updatedStaff_status = await Staff_statusDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedStaff_status; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Staff_statusDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Staff_statusDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/staffs.js b/backend/src/services/staffs.js new file mode 100644 index 0000000..f3f3a13 --- /dev/null +++ b/backend/src/services/staffs.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const StaffsDBApi = require('../db/api/staffs'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class StaffsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await StaffsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await StaffsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let staffs = await StaffsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!staffs) { + throw new ValidationError( + 'staffsNotFound', + ); + } + + const updatedStaffs = await StaffsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedStaffs; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await StaffsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await StaffsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/urls.js b/backend/src/services/urls.js new file mode 100644 index 0000000..bc13c0d --- /dev/null +++ b/backend/src/services/urls.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const UrlsDBApi = require('../db/api/urls'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class UrlsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await UrlsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await UrlsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let urls = await UrlsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!urls) { + throw new ValidationError( + 'urlsNotFound', + ); + } + + const updatedUrls = await UrlsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedUrls; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await UrlsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await UrlsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/user_has_menu.js b/backend/src/services/user_has_menu.js new file mode 100644 index 0000000..bacdfdb --- /dev/null +++ b/backend/src/services/user_has_menu.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const User_has_menuDBApi = require('../db/api/user_has_menu'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class User_has_menuService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await User_has_menuDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await User_has_menuDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let user_has_menu = await User_has_menuDBApi.findBy( + {id}, + {transaction}, + ); + + if (!user_has_menu) { + throw new ValidationError( + 'user_has_menuNotFound', + ); + } + + const updatedUser_has_menu = await User_has_menuDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedUser_has_menu; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await User_has_menuDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await User_has_menuDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/user_type_urls.js b/backend/src/services/user_type_urls.js new file mode 100644 index 0000000..63ea9cd --- /dev/null +++ b/backend/src/services/user_type_urls.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const User_type_urlsDBApi = require('../db/api/user_type_urls'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class User_type_urlsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await User_type_urlsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await User_type_urlsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let user_type_urls = await User_type_urlsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!user_type_urls) { + throw new ValidationError( + 'user_type_urlsNotFound', + ); + } + + const updatedUser_type_urls = await User_type_urlsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedUser_type_urls; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await User_type_urlsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await User_type_urlsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/user_types.js b/backend/src/services/user_types.js new file mode 100644 index 0000000..29c080c --- /dev/null +++ b/backend/src/services/user_types.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const User_typesDBApi = require('../db/api/user_types'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class User_typesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await User_typesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await User_typesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let user_types = await User_typesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!user_types) { + throw new ValidationError( + 'user_typesNotFound', + ); + } + + const updatedUser_types = await User_typesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedUser_types; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await User_typesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await User_typesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/users.js b/backend/src/services/users.js new file mode 100644 index 0000000..ceca8b8 --- /dev/null +++ b/backend/src/services/users.js @@ -0,0 +1,163 @@ +const db = require('../db/models'); +const UsersDBApi = require('../db/api/users'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +const AuthService = require('./auth'); + +module.exports = class UsersService { + static async create(data, currentUser, sendInvitationEmails = true, host) { + let transaction = await db.sequelize.transaction(); + let email = data.email; + let emailsToInvite = []; + try { + if (email) { + let user = await UsersDBApi.findBy({email}, {transaction}); + if (user) { + throw new ValidationError( + 'iam.errors.userAlreadyExists', + ); + } else { + await UsersDBApi.create( + {data}, + { + currentUser, + transaction, + }, + ); + emailsToInvite.push(email); + } + } else { + throw new ValidationError('iam.errors.emailRequired') + } + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + if (emailsToInvite && emailsToInvite.length) { + if (!sendInvitationEmails) return; + + AuthService.sendPasswordResetEmail(email, 'invitation', host); + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + let emailsToInvite = []; + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', () => { + console.log('results csv', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + const hasAllEmails = results.every((result) => result.email); + + if (!hasAllEmails) { + throw new ValidationError('importer.errors.userEmailMissing'); + } + + await UsersDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + emailsToInvite = results.map((result) => result.email); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + + if (emailsToInvite && emailsToInvite.length && !sendInvitationEmails) { + + emailsToInvite.forEach((email) => { + AuthService.sendPasswordResetEmail(email, 'invitation', host); + }); + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let users = await UsersDBApi.findBy( + {id}, + {transaction}, + ); + + if (!users) { + throw new ValidationError( + 'iam.errors.userNotFound', + ); + } + + const updatedUser = await UsersDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedUser; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + if (currentUser.id === id) { + throw new ValidationError( + 'iam.errors.deletingHimself', + ); + } + +// if (currentUser.app_role?.name !== config.roles.admin) { +// throw new ValidationError( +// 'errors.forbidden.message', +// ); +// } + + await UsersDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/villages.js b/backend/src/services/villages.js new file mode 100644 index 0000000..b00919b --- /dev/null +++ b/backend/src/services/villages.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const VillagesDBApi = require('../db/api/villages'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class VillagesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await VillagesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await VillagesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let villages = await VillagesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!villages) { + throw new ValidationError( + 'villagesNotFound', + ); + } + + const updatedVillages = await VillagesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedVillages; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await VillagesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await VillagesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/watcher.js b/backend/watcher.js new file mode 100644 index 0000000..c9218d5 --- /dev/null +++ b/backend/watcher.js @@ -0,0 +1,45 @@ +const chokidar = require('chokidar'); +const { exec } = require('child_process'); +const nodemon = require('nodemon'); + +const migrationsWatcher = chokidar.watch('./src/db/migrations', { + persistent: true, + ignoreInitial: true +}); +migrationsWatcher.on('add', (filePath) => { + console.log(`[DEBUG] New migration file: ${filePath}`); + exec('npm run db:migrate', (error, stdout, stderr) => { + console.log(stdout); + if (error) { + console.error(stderr); + } + }); +}); + +const seedersWatcher = chokidar.watch('./src/db/seeders', { + persistent: true, + ignoreInitial: true +}); +seedersWatcher.on('add', (filePath) => { + console.log(`[DEBUG] New seed file: ${filePath}`); + exec('npm run db:seed', (error, stdout, stderr) => { + console.log(stdout); + if (error) { + console.error(stderr); + } + }); +}); + +nodemon({ + script: './src/index.js', + ignore: ['./src/db/migrations', './src/db/seeders'], + delay: '500' +}); + +nodemon.on('start', () => { + console.log('Nodemon started'); +}); + +nodemon.on('restart', (files) => { + console.log('Nodemon restarted due changes in:', files); +}); \ No newline at end of file diff --git a/backend/yarn.lock b/backend/yarn.lock new file mode 100644 index 0000000..222a4f9 --- /dev/null +++ b/backend/yarn.lock @@ -0,0 +1,4470 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz#8ff5386b365d4c9faa7c8b566ff16a46a577d9b8" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + +"@azure/abort-controller@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249" + integrity sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw== + dependencies: + tslib "^2.2.0" + +"@azure/abort-controller@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" + integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== + dependencies: + tslib "^2.6.2" + +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.5.0": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.7.2.tgz#558b7cb7dd12b00beec07ae5df5907d74df1ebd9" + integrity sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.1.0" + tslib "^2.6.2" + +"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74" + integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.4.0" + "@azure/core-rest-pipeline" "^1.9.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.6.1" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-http-compat@^2.0.1": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz#d1585ada24ba750dc161d816169b33b35f762f0d" + integrity sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-client" "^1.3.0" + "@azure/core-rest-pipeline" "^1.3.0" + +"@azure/core-lro@^2.2.0": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-2.7.2.tgz#787105027a20e45c77651a98b01a4d3b01b75a08" + integrity sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.2.0" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-paging@^1.1.1": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@azure/core-paging/-/core-paging-1.6.2.tgz#40d3860dc2df7f291d66350b2cfd9171526433e7" + integrity sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA== + dependencies: + tslib "^2.6.2" + +"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.3.0", "@azure/core-rest-pipeline@^1.8.1", "@azure/core-rest-pipeline@^1.9.1": + version "1.16.2" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.2.tgz#3f71b09e45a65926cc598478b4f1bcd0fe67bf4b" + integrity sha512-Hnhm/PG9/SQ07JJyLDv3l9Qr8V3xgAe1hFoBYzt6LaalMxfL/ZqFaZf/bz5VN3pMcleCPwl8ivlS2Fjxq/iC8Q== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.4.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.9.0" + "@azure/logger" "^1.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + +"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.1.2.tgz#065dab4e093fb61899988a1cdbc827d9ad90b4ee" + integrity sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA== + dependencies: + tslib "^2.6.2" + +"@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.3.0", "@azure/core-util@^1.6.1", "@azure/core-util@^1.9.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.1.tgz#05ea9505c5cdf29c55ccf99a648c66ddd678590b" + integrity sha512-OLsq0etbHO1MA7j6FouXFghuHrAFGk+5C1imcpQ2e+0oZhYF07WLA+NW2Vqs70R7d+zOAWiWM3tbE1sXcDN66g== + dependencies: + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" + +"@azure/identity@^4.2.1": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.4.0.tgz#f2743e63d346000a70b0eed5a3b397dedd3984a7" + integrity sha512-oG6oFNMxUuoivYg/ElyZWVSZfw42JQyHbrp+lR7VJ1BYWsGzt34NwyDw3miPp1QI7Qm5+4KAd76wGsbHQmkpkg== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.5.0" + "@azure/core-client" "^1.9.2" + "@azure/core-rest-pipeline" "^1.1.0" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.3.0" + "@azure/logger" "^1.0.0" + "@azure/msal-browser" "^3.14.0" + "@azure/msal-node" "^2.9.2" + events "^3.0.0" + jws "^4.0.0" + open "^8.0.0" + stoppable "^1.1.0" + tslib "^2.2.0" + +"@azure/keyvault-keys@^4.4.0": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@azure/keyvault-keys/-/keyvault-keys-4.8.0.tgz#1513b3a187bb3a9a372b5980c593962fb793b2ad" + integrity sha512-jkuYxgkw0aaRfk40OQhFqDIupqblIOIlYESWB6DKCVDxQet1pyv86Tfk9M+5uFM0+mCs6+MUHU+Hxh3joiUn4Q== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-client" "^1.5.0" + "@azure/core-http-compat" "^2.0.1" + "@azure/core-lro" "^2.2.0" + "@azure/core-paging" "^1.1.1" + "@azure/core-rest-pipeline" "^1.8.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.0.0" + "@azure/logger" "^1.0.0" + tslib "^2.2.0" + +"@azure/logger@^1.0.0": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.1.3.tgz#09a8fd4850b9112865756e92d5e8b728ee457345" + integrity sha512-J8/cIKNQB1Fc9fuYqBVnrppiUtW+5WWJPCj/tAokC5LdSTwkWWttN+jsRgw9BLYD7JDBx7PceiqOBxJJ1tQz3Q== + dependencies: + tslib "^2.6.2" + +"@azure/msal-browser@^3.14.0": + version "3.19.1" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.19.1.tgz#c5e5a7996f95cadc11920bffa2bf6321e3a24555" + integrity sha512-pqYP2gK0GCEa4OxtOqlS+EdFQqhXV6ZuESgSTYWq2ABXyxBVVdd5KNuqgR5SU0OwI2V1YWdFVvLDe1487dyQ0g== + dependencies: + "@azure/msal-common" "14.13.1" + +"@azure/msal-common@14.13.1": + version "14.13.1" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.13.1.tgz#e296cf8cc556082af9c35d803496424e8a95d8b7" + integrity sha512-iUp3BYrsRZ4X3EiaZ2fDjNFjmtYMv9rEQd6c1op6ULn0HWk4ACvDmosL6NaBgWOhl1BAblIbd9vmB5/ilF8d4A== + +"@azure/msal-node@^2.9.2": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.11.1.tgz#7fea67a1c6904301eb8853fae7df86c34306a9cc" + integrity sha512-8ECtug4RL+zsgh20VL8KYHjrRO3MJOeAKEPRXT2lwtiu5U3BdyIdBb50+QZthEkIi60K6pc/pdOx/k5Jp4sLng== + dependencies: + "@azure/msal-common" "14.13.1" + jsonwebtoken "^9.0.0" + uuid "^8.3.0" + +"@google-cloud/paginator@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" + integrity sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/projectify@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.1.1.tgz#ae6af4fee02d78d044ae434699a630f8df0084ef" + integrity sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ== + +"@google-cloud/promisify@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.4.tgz#9d8705ecb2baa41b6b2673f3a8e9b7b7e1abc52a" + integrity sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA== + +"@google-cloud/storage@^5.18.2": + version "5.20.5" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-5.20.5.tgz#1de71fc88d37934a886bc815722c134b162d335d" + integrity sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw== + dependencies: + "@google-cloud/paginator" "^3.0.7" + "@google-cloud/projectify" "^2.0.0" + "@google-cloud/promisify" "^2.0.0" + abort-controller "^3.0.0" + arrify "^2.0.0" + async-retry "^1.3.3" + compressible "^2.0.12" + configstore "^5.0.0" + duplexify "^4.0.0" + ent "^2.2.0" + extend "^3.0.2" + gaxios "^4.0.0" + google-auth-library "^7.14.1" + hash-stream-validation "^0.2.2" + mime "^3.0.0" + mime-types "^2.0.8" + p-limit "^3.0.1" + pumpify "^2.0.0" + retry-request "^4.2.2" + stream-events "^1.0.4" + teeny-request "^7.1.3" + uuid "^8.0.0" + xdg-basedir "^4.0.0" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@js-joda/core@^5.6.1": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-5.6.3.tgz#41ae1c07de1ebe0f6dde1abcbc9700a09b9c6056" + integrity sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA== + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@one-ini/wasm@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" + integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/debug@^4.1.8": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + +"@types/node@*", "@types/node@>=18": + version "20.14.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b" + integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA== + dependencies: + undici-types "~5.26.4" + +"@types/readable-stream@^4.0.0": + version "4.0.15" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-4.0.15.tgz#e6ec26fe5b02f578c60baf1fa9452e90957d2bfb" + integrity sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + +"@types/validator@^13.7.17": + version "13.12.0" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.0.tgz#1fe4c3ae9de5cf5193ce64717c99ef2fa7d8756f" + integrity sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@^1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.1, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array.prototype.map@^1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.7.tgz#82fa4d6027272d1fca28a63bbda424d0185d78a7" + integrity sha512-XpcFfLoBEAhezrrNw1V+yLXkE7M6uR7xJEsxbG6c/V9v043qurwVJB9r9UTnoSioFDoz1i1VOydpWGmJpfVZbg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axios@^1.6.7: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.0, base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base64url@3.x.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +bcrypt@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^6.0.11: + version "6.0.14" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.0.14.tgz#b9ae9862118a3d2ebec999c5318466012314f96c" + integrity sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ== + dependencies: + "@types/readable-stream" "^4.0.0" + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^4.2.0" + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg== + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +chokidar@^3.2.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cli-boxes@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-color@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.4.tgz#d658080290968816b322248b7306fad2346fb2c8" + integrity sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA== + dependencies: + d "^1.0.1" + es5-ext "^0.10.64" + es6-iterator "^2.0.3" + memoizee "^0.4.15" + timers-ext "^0.1.7" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +compressible@^2.0.12: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^5.0.0, configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-env@7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.1, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^3.2.6: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== + dependencies: + mimic-response "^1.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +denque@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg== + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + +diff@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dottie@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4" + integrity sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA== + +duplexer3@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" + integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + +duplexify@^4.0.0, duplexify@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +editorconfig@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.4.tgz#040c9a8e9a6c5288388b87c2db07028aa89f53a3" + integrity sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q== + dependencies: + "@one-ini/wasm" "0.1.1" + commander "^10.0.0" + minimatch "9.0.1" + semver "^7.5.3" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +ent@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" + integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== + dependencies: + punycode "^1.4.1" + +es-abstract@^1.17.0-next.1, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.0.0, events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-text-encoding@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== + dependencies: + is-buffer "~2.0.3" + +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2, fresh@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gaxios@^4.0.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.3.tgz#d44bdefe52d34b6435cc41214fdb160b64abfc22" + integrity sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.7" + +gcp-metadata@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9" + integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A== + dependencies: + gaxios "^4.0.0" + json-bigint "^1.0.0" + +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +glob-parent@~5.1.0, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^10.3.3: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" + integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== + dependencies: + ini "1.3.7" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +google-auth-library@^7.14.1: + version "7.14.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.14.1.tgz#e3483034162f24cc71b95c8a55a210008826213c" + integrity sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-p12-pem@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" + integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== + dependencies: + node-forge "^1.3.1" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +gtoken@^5.0.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" + integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== + dependencies: + gaxios "^4.0.0" + google-p12-pem "^3.1.3" + jws "^4.0.0" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.0, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +hash-stream-validation@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" + integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +helmet@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.1.1.tgz#751f0e273d809ace9c172073e0003bed27d27a4a" + integrity sha512-Avg4XxSBrehD94mkRwEljnO+6RZx7AGfk8Wa6K1nxaU+hbXlFOhlOIMgPfFqOYQB/dBCsTpootTGuiOG+CHiQA== + +http-cache-semantics@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.2, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflection@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.13.4.tgz#65aa696c4e2da6225b148d7a154c449366633a32" + integrity sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + +ini@^1.3.4, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-map@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterate-iterator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" + integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== + +iterate-value@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== + dependencies: + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +js-beautify@^1.14.5: + version "1.15.1" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.1.tgz#4695afb508c324e1084ee0b952a102023fc65b64" + integrity sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA== + dependencies: + config-chain "^1.1.13" + editorconfig "^1.0.4" + glob "^10.3.3" + js-cookie "^3.0.5" + nopt "^7.2.0" + +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + +js-md4@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/js-md4/-/js-md4-0.3.2.tgz#cd3b3dc045b0c404556c81ddb5756c23e59d7cf5" + integrity sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA== + +js-yaml@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== + +json2csv@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-5.0.7.tgz#f3a583c25abd9804be873e495d1e65ad8d1b54ae" + integrity sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA== + dependencies: + commander "^6.1.0" + jsonparse "^1.3.1" + lodash.get "^4.4.2" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +jsonwebtoken@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@4.17.21, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + +make-dir@^3.0.0, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memoizee@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949" + integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA== + dependencies: + d "^1.0.2" + es5-ext "^0.10.64" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-descriptors@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.3.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.4.2" + debug "4.1.1" + diff "4.0.2" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.1.6" + growl "1.10.5" + he "1.2.0" + js-yaml "3.14.0" + log-symbols "4.0.0" + minimatch "3.0.4" + ms "2.1.2" + object.assign "4.1.0" + promise.allsettled "1.0.2" + serialize-javascript "4.0.0" + strip-json-comments "3.0.1" + supports-color "7.1.0" + which "2.0.2" + wide-align "1.1.3" + workerpool "6.0.0" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.1" + +moment-timezone@^0.5.43: + version "0.5.45" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" + integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== + dependencies: + moment "^2.29.4" + +moment@2.30.1, moment@^2.29.4: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c" + integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + +mysql2@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.2.5.tgz#72624ffb4816f80f96b9c97fedd8c00935f9f340" + integrity sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g== + dependencies: + denque "^1.4.1" + generate-function "^2.3.1" + iconv-lite "^0.6.2" + long "^4.0.0" + lru-cache "^6.0.0" + named-placeholders "^1.1.2" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + +named-placeholders@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== + dependencies: + lru-cache "^7.14.1" + +native-duplexpair@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz#7899078e64bf3c8a3d732601b3d40ff05db58fa0" + integrity sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-mocks-http@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.9.0.tgz#6000c570fc4b809603782309be81c73a71d85b71" + integrity sha512-ILf7Ws8xyX9Rl2fLZ7xhZBovrRwgaP84M13esndP6V17M/8j25TpwNzb7Im8U9XCo6fRhdwqiQajWXpsas/E6w== + dependencies: + accepts "^1.3.7" + depd "^1.1.0" + fresh "^0.5.2" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mime "^1.3.4" + parseurl "^1.3.3" + range-parser "^1.2.0" + type-is "^1.6.18" + +nodemailer@6.9.9: + version "6.9.9" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.9.tgz#4549bfbf710cc6addec5064dd0f19874d24248d9" + integrity sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA== + +nodemon@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.5.tgz#df67fe1fd1312ddb0c1e393ae2cf55aacdcec2f3" + integrity sha512-6/jqtZvJdk092pVnD2AIH19KQ9GQZAKOZVy/yT1ueL6aoV+Ix7a1lVZStXzvEh0fP4zE41DDWlkVoHjR6WlozA== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.3" + update-notifier "^4.1.0" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@^7.2.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== + dependencies: + abbrev "^2.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +oauth@0.10.x: + version "0.10.0" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58" + integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== + +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-keys@^1.0.11, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +on-finished@2.4.1, on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.1, p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + +parseurl@^1.3.3, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +passport-google-oauth2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz#fc9ea59e7091f02e24fd16d6be9257ea982ebbc3" + integrity sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ== + dependencies: + passport-oauth2 "^1.1.2" + +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-microsoft@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/passport-microsoft/-/passport-microsoft-0.1.0.tgz#dc72c1a38b294d74f4dc55fe93f52e25cb9aa5b4" + integrity sha512-0giBDgE1fnR5X84zJZkQ11hnKVrzEgViwRO6RGsormK9zTxFQmN/UHMTDbIpvhk989VqALewB6Pk1R5vNr3GHw== + dependencies: + passport-oauth2 "1.2.0" + pkginfo "0.2.x" + +passport-oauth2@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.2.0.tgz#49613a3eca85c7a1e65bf1019e2b6b80a10c8ac2" + integrity sha512-6128N+n/MOrJdXxdC2q/PVKXtqgihGFIeup+9bsPybAvMPOUKqdGhh9ZIzZF8rFKJOlxUP9fgP3H0JQe18n0rg== + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + +passport-oauth2@^1.1.2: + version "1.8.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8" + integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== + dependencies: + base64url "3.x.x" + oauth "0.10.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +pg-connection-string@^2.4.0, pg-connection-string@^2.6.1: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-hstore@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd" + integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA== + dependencies: + underscore "^1.13.1" + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.2.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@^1.3.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.4.1.tgz#06cfb6208ae787a869b2f0022da11b90d13d933e" + integrity sha512-NRsH0aGMXmX1z8Dd0iaPCxWUw4ffu+lIAmGm+sTCwuDDWkpEgRCAHZYDwqaNhC5hG5DRMOjSUFasMWhvcmLN1A== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.4.0" + pg-pool "^3.2.1" + pg-protocol "^1.3.0" + pg-types "^2.1.0" + pgpass "1.x" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkginfo@0.2.x: + version "0.2.3" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" + integrity sha512-7W7wTrE/NsY8xv/DTGjwNIyNah81EQH0MWcTzrHL6pOpMocOGZc0Mbdz9aXxSrp+U0mSmkU8jrNCDCfUs3sOBg== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +promise.allsettled@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" + integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== + dependencies: + array.prototype.map "^1.0.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + iterate-value "^1.0.0" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" + integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== + dependencies: + duplexify "^4.1.1" + inherits "^2.0.3" + pump "^3.0.0" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +pupa@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.0, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@1.2.8, rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.2.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +registry-auth-token@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" + integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== + dependencies: + rc "1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== + dependencies: + lowercase-keys "^1.0.0" + +retry-as-promised@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.4.tgz#9df73adaeea08cb2948b9d34990549dc13d800a2" + integrity sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA== + +retry-request@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.2.2.tgz#b7d82210b6d2651ed249ba3497f07ea602f1a903" + integrity sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg== + dependencies: + debug "^4.1.1" + extend "^3.0.2" + +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +semver@^5.6.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + +sequelize-cli@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-6.6.2.tgz#8d838b25c988cf136914cdc3843e19d88c3dcb67" + integrity sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg== + dependencies: + cli-color "^2.0.3" + fs-extra "^9.1.0" + js-beautify "^1.14.5" + lodash "^4.17.21" + resolve "^1.22.1" + umzug "^2.3.0" + yargs "^16.2.0" + +sequelize-json-schema@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sequelize-json-schema/-/sequelize-json-schema-2.1.1.tgz#a82d3813925e81485d76ce291f4ff5c8cb2ae492" + integrity sha512-yCGaHnmQQeL6MQ/fOxhkR5C2aOGZyTD6OrgjP4yw1rbuujuIUVdzWN3AsC6r6AvlGZ3EUBBbCJHKl8OIFFES4Q== + +sequelize-pool@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz#210b391af4002762f823188fd6ecfc7413020768" + integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== + +sequelize@6.35.2: + version "6.35.2" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.35.2.tgz#9276d24055a9a07bd6812c89ab402659f5853e70" + integrity sha512-EdzLaw2kK4/aOnWQ7ed/qh3B6/g+1DvmeXr66RwbcqSm/+QRS9X0LDI5INBibsy4eNJHWIRPo3+QK0zL+IPBHg== + dependencies: + "@types/debug" "^4.1.8" + "@types/validator" "^13.7.17" + debug "^4.3.4" + dottie "^2.0.6" + inflection "^1.13.4" + lodash "^4.17.21" + moment "^2.29.4" + moment-timezone "^0.5.43" + pg-connection-string "^2.6.1" + retry-as-promised "^7.0.4" + semver "^7.5.4" + sequelize-pool "^7.1.0" + toposort-class "^1.0.1" + uuid "^8.3.2" + validator "^13.9.0" + wkx "^0.5.0" + +serialize-javascript@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +sqlite@4.0.15: + version "4.0.15" + resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-4.0.15.tgz#071e0577afb327fbd74a75354ea15964378392e3" + integrity sha512-irPPTrbVoDvwzRGpe0v8vxpNwMl+q0tXQzffQTcCUnaJzQFO0hfLLvFwGDKxd6vYBuvEr3uvPkObVoGOvVsmzA== + +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +stoppable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" + integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== + +stream-events@^1.0.4, stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + +supports-color@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz#6d33d9fb07ff4a7c1564379c52c08989ec7d0256" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.3.tgz#04cb01c18c3ac192b41161c77f81e79309135d03" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.17.14" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz#e2c222e5bf9e15ccf80ec4bc08b4aaac09792fd6" + integrity sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw== + +swagger-ui-express@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz#fb8c1b781d2793a6bd2f8a205a3f4bd6fa020dd8" + integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== + dependencies: + swagger-ui-dist ">=5.0.0" + +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tedious@^18.2.4: + version "18.2.4" + resolved "https://registry.yarnpkg.com/tedious/-/tedious-18.2.4.tgz#c33986f2561b4fde92bb9df70f44ae1a14f71b46" + integrity sha512-+6Nzn/aURTQ+8OxLAJ8fKK5Fbb84HRTI3bHiAC3ZzBKrBg9BHtcHxjmlIni5Zn46hzKiZ5WrDMSwDH8oIYjV8w== + dependencies: + "@azure/identity" "^4.2.1" + "@azure/keyvault-keys" "^4.4.0" + "@js-joda/core" "^5.6.1" + "@types/node" ">=18" + bl "^6.0.11" + iconv-lite "^0.6.3" + js-md4 "^0.3.2" + native-duplexpair "^1.0.0" + sprintf-js "^1.1.3" + +teeny-request@^7.1.3: + version "7.2.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.2.0.tgz#41347ece068f08d741e7b86df38a4498208b2633" + integrity sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.1" + stream-events "^1.0.5" + uuid "^8.0.0" + +term-size@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + +timers-ext@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== + dependencies: + es5-ext "^0.10.64" + next-tick "^1.1.0" + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toposort-class@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" + integrity sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tslib@^2.2.0, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@^1.6.18, type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +uid2@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" + integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== + +umzug@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/umzug/-/umzug-2.3.0.tgz#0ef42b62df54e216b05dcaf627830a6a8b84a184" + integrity sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw== + dependencies: + bluebird "^3.7.2" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undefsafe@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +underscore@^1.13.1: + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-notifier@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" + integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== + dependencies: + prepend-http "^2.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +validator@^13.7.0, validator@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + +workerpool@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" + integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^15.0.1: + version "15.0.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.3.tgz#316e263d5febe8b38eef61ac092b33dfcc9b1115" + integrity sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" + integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== + dependencies: + camelcase "^5.3.1" + decamelize "^1.2.0" + flat "^4.1.0" + is-plain-obj "^1.1.0" + yargs "^14.2.3" + +yargs@13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yargs@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" + integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== + dependencies: + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.1" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +z-schema@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.6.tgz#46d6a687b15e4a4369e18d6cb1c7b8618fc256c5" + integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^10.0.0" diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..7cf4d9d --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,14 @@ +steps: + - name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: ['-c', 'docker pull gcr.io/fldemo-315215/meng-leap-cash-32693-dev:latest || exit 0'] + - name: 'gcr.io/cloud-builders/docker' + args: [ + 'build', + '-t', 'gcr.io/fldemo-315215/meng-leap-cash-32693-dev:latest', + '--file', 'Dockerfile.dev', + '--cache-from', 'gcr.io/fldemo-315215/meng-leap-cash-32693-dev:latest', + '.' + ] +images: ['gcr.io/fldemo-315215/meng-leap-cash-32693-dev:latest'] +logsBucket: 'gs://fldemo-315215-cloudbuild-logs' \ No newline at end of file diff --git a/community-template.png b/community-template.png new file mode 100644 index 0000000..e653a2c Binary files /dev/null and b/community-template.png differ diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..adbb97d --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1 @@ +data/ \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..69d1021 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,46 @@ +## Description: + + The project contains the **docker folder** and the `Dockerfile`. + + The `Dockerfile` is used to Deploy the project to Google Cloud. + + The **docker folder** contains a couple of helper scripts: + +- `docker-compose.yml` (all our services: web, backend, db are described here) +- `start-backend.sh` (starts backend, but only after the database) +- `wait-for-it.sh` (imported from https://github.com/vishnubob/wait-for-it) + + > To avoid breaking the application, we recommend you don't edit the following files: everything that includes the **docker folder** and `Dokerfile`. + + + ## Run services: + + 1. Install docker compose (https://docs.docker.com/compose/install/) + + 2. Move to `docker` folder. All next steps should be done from this folder. + + ``` cd docker ``` + + 3. Make executables from `wait-for-it.sh` and `start-backend.sh`: + + ``` chmod +x start-backend.sh && chmod +x wait-for-it.sh ``` + + 4. Download dependend projects for services. + + 5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. + + 6. Make sure you have needed ports (see them in `ports`) available on your local machine. + + 7. Start services: + + 7.1. With an empty database `rm -rf data && docker-compose up` + + 7.2. With a stored (from previus runs) database data `docker-compose up` + + 8. Check http://localhost:3000 + + 9. Stop services: + + 9.1. Just press `Ctr+C` + + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..1b2acaf --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,35 @@ + + +version: "3.9" +services: + web: + image: frontend + build: ../frontend + stdin_open: true # docker run -i + tty: true # docker run -t + ports: + - "3000:3000" + db: + image: postgres + volumes: + - ./data/db:/var/lib/postgresql/data + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + - POSTGRES_DB=db_meng_leap_cash + ports: + - "5432:5432" + backend: + image: backend + volumes: + - ./wait-for-it.sh:/usr/src/app/wait-for-it.sh + - ./start-backend.sh:/usr/src/app/start-backend.sh + build: ../backend + environment: + - DB_HOST=db + ports: + - "8080:8080" + depends_on: + - "db" + + command: ["bash", "./wait-for-it.sh", "db:5432", "--timeout=0", "--strict", "--", "bash", "./start-backend.sh"] + diff --git a/docker/start-backend.sh b/docker/start-backend.sh new file mode 100644 index 0000000..fb353bf --- /dev/null +++ b/docker/start-backend.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +yarn start diff --git a/docker/wait-for-it.sh b/docker/wait-for-it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/docker/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..56e10d0 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20.15.1-alpine + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + +# Bundle app source +COPY . . + +EXPOSE 3000 +CMD [ "yarn", "dev" ] \ No newline at end of file diff --git a/frontend/LICENSE-justboil b/frontend/LICENSE-justboil new file mode 100644 index 0000000..798238d --- /dev/null +++ b/frontend/LICENSE-justboil @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019-current JustBoil.me (https://justboil.me) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7858443 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,89 @@ +# meng-leap-cash + +## This project was generated by Flatlogic Platform. +## Install + +`cd` to project's dir and run `npm install` + +### Builds + +Build are handled by Next.js CLI — [Info](https://nextjs.org/docs/api-reference/cli) + +### Hot-reloads for development + +``` +npm run dev +``` + +### Builds and minifies for production + +``` +npm run build +``` + +### Exports build for static hosts + +``` +npm run export +``` + +### Lint + +``` +npm run lint +``` + +### Format with prettier + +``` +npm run format +``` + +## Support +For any additional information please refer to [Flatlogic homepage](https://flatlogic.com). + +## To start the project with Docker: +### Description: + +The project contains the **docker folder** and the `Dockerfile`. + +The `Dockerfile` is used to Deploy the project to Google Cloud. + +The **docker folder** contains a couple of helper scripts: + +- `docker-compose.yml` (all our services: web, backend, db are described here) +- `start-backend.sh` (starts backend, but only after the database) +- `wait-for-it.sh` (imported from https://github.com/vishnubob/wait-for-it) + + > To avoid breaking the application, we recommend you don't edit the following files: everything that includes the **docker folder** and `Dokerfile`. + +### Run services: + +1. Install docker compose (https://docs.docker.com/compose/install/) + +2. Move to `docker` folder. All next steps should be done from this folder. + + ``` cd docker ``` + +3. Make executables from `wait-for-it.sh` and `start-backend.sh`: + + ``` chmod +x start-backend.sh && chmod +x wait-for-it.sh ``` + +4. Download dependend projects for services. + +5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. + +6. Make sure you have needed ports (see them in `ports`) available on your local machine. + +7. Start services: + + 7.1. With an empty database `rm -rf data && docker-compose up` + + 7.2. With a stored (from previus runs) database data `docker-compose up` + +8. Check http://localhost:3000 + +9. Stop services: + + 9.1. Just press `Ctr+C` + diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..52e831b --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..c5686db --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,28 @@ +/** + * @type {import('next').NextConfig} + */ + +const output = process.env.NODE_ENV === 'production' ? 'export' : 'standalone'; + const nextConfig = { +trailingSlash: true, + distDir: 'build', + output, + basePath: "", + devIndicators: { + position: 'bottom-left', + }, + typescript: { + ignoreBuildErrors: true, + }, + images: { + unoptimized: true, + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + }, +} + +export default nextConfig diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9fcc9f3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,66 @@ +{ + "private": true, + "scripts": { + "dev": "cross-env PORT=${FRONT_PORT:-3000} next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "prettier '{components,pages,src,interfaces,hooks}/**/*.{tsx,ts,js}' --write" + }, + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mdi/js": "^7.4.47", + "@mui/material": "^6.3.0", + "@mui/x-data-grid": "^6.19.2", + "@reduxjs/toolkit": "^2.1.0", + "@tailwindcss/typography": "^0.5.13", + "@tinymce/tinymce-react": "^4.3.2", + "axios": "^1.8.4", + "chroma-js": "^2.4.2", + "dayjs": "^1.11.10", + "file-saver": "^2.0.5", + "formik": "^2.4.5", + "intro.js": "^7.2.0", + "intro.js-react": "^1.0.0", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "next": "^15.3.1", + "numeral": "^2.0.6", + "query-string": "^8.1.0", + "react": "^19.0.0", + "react-datepicker": "^4.10.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^19.0.0", + "react-redux": "^8.0.2", + "react-select": "^5.7.0", + "react-select-async-paginate": "^0.7.9", + "react-switch": "^7.0.0", + "react-toastify": "^11.0.2", + "swr": "^1.3.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/line-clamp": "^0.4.4", + "@types/node": "18.7.16", + "@types/numeral": "^2.0.2", + "@types/react-big-calendar": "^1.8.8", + "@types/react-redux": "^7.1.24", + "@typescript-eslint/eslint-plugin": "^5.37.0", + "@typescript-eslint/parser": "^5.37.0", + "autoprefixer": "^10.4.0", + "cross-env": "^7.0.3", + "eslint": "^8.23.1", + "eslint-config-next": "^13.0.4", + "eslint-config-prettier": "^8.5.0", + "postcss": "^8.4.4", + "postcss-import": "^14.1.0", + "prettier": "^3.2.4", + "tailwindcss": "^3.4.1", + "typescript": "^5.4.5" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..36f40f3 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,9 @@ +/* eslint-env node */ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + } +} \ No newline at end of file diff --git a/frontend/prettier.config.js b/frontend/prettier.config.js new file mode 100644 index 0000000..ecde36c --- /dev/null +++ b/frontend/prettier.config.js @@ -0,0 +1,13 @@ +module.exports = { + semi: false, + singleQuote: true, + printWidth: 100, + trailingComma: 'es5', + arrowParens: 'always', + tabWidth: 2, + useTabs: false, + quoteProps: 'as-needed', + jsxSingleQuote: false, + bracketSpacing: true, + bracketSameLine: false, +} \ No newline at end of file diff --git a/frontend/public/data-sources/clients.json b/frontend/public/data-sources/clients.json new file mode 100644 index 0000000..73287b0 --- /dev/null +++ b/frontend/public/data-sources/clients.json @@ -0,0 +1 @@ +{"data":[{"id":19,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Howell-Hand.svg","login":"percy64","name":"Howell Hand","company":"Kiehn-Green","city":"Emelyside","progress":70,"created":"Mar 3, 2022","created_mm_dd_yyyy":"03-03-2022"},{"id":11,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Hope-Howe.svg","login":"dare.concepcion","name":"Hope Howe","company":"Nolan Inc","city":"Paristown","progress":68,"created":"Dec 1, 2022","created_mm_dd_yyyy":"12-01-2022"},{"id":32,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Nelson-Jerde.svg","login":"geovanni.kessler","name":"Nelson Jerde","company":"Nitzsche LLC","city":"Jailynbury","progress":49,"created":"May 18, 2022","created_mm_dd_yyyy":"05-18-2022"},{"id":22,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Kim-Weimann.svg","login":"macejkovic.dashawn","name":"Kim Weimann","company":"Brown-Lueilwitz","city":"New Emie","progress":38,"created":"May 4, 2022","created_mm_dd_yyyy":"05-04-2022"},{"id":34,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Justice-OReilly.svg","login":"hilpert.leora","name":"Justice O'Reilly","company":"Lakin-Muller","city":"New Kacie","progress":38,"created":"Mar 27, 2022","created_mm_dd_yyyy":"03-27-2022"},{"id":48,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Adrienne-Mayer-III.svg","login":"ferry.sophia","name":"Adrienne Mayer III","company":"Kozey, McLaughlin and Kuhn","city":"Howardbury","progress":39,"created":"Mar 29, 2022","created_mm_dd_yyyy":"03-29-2022"},{"id":20,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Mr.-Julien-Ebert.svg","login":"gokuneva","name":"Mr. Julien Ebert","company":"Cormier LLC","city":"South Serenaburgh","progress":29,"created":"Jun 25, 2022","created_mm_dd_yyyy":"06-25-2022"},{"id":47,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Lenna-Smitham.svg","login":"paolo.walter","name":"Lenna Smitham","company":"King Inc","city":"McCulloughfort","progress":59,"created":"Oct 8, 2022","created_mm_dd_yyyy":"10-08-2022"},{"id":24,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Travis-Davis.svg","login":"lkessler","name":"Travis Davis","company":"Leannon and Sons","city":"West Frankton","progress":52,"created":"Oct 20, 2022","created_mm_dd_yyyy":"10-20-2022"},{"id":49,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Prof.-Esteban-Steuber.svg","login":"shana.lang","name":"Prof. Esteban Steuber","company":"Langosh-Ernser","city":"East Sedrick","progress":34,"created":"May 16, 2022","created_mm_dd_yyyy":"05-16-2022"},{"id":36,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Russell-Goodwin-V.svg","login":"jewel07","name":"Russell Goodwin V","company":"Nolan-Stracke","city":"Williamsonmouth","progress":55,"created":"Apr 22, 2022","created_mm_dd_yyyy":"04-22-2022"},{"id":33,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Ms.-Cassidy-Wiegand-DVM.svg","login":"burnice.okuneva","name":"Ms. Cassidy Wiegand DVM","company":"Kuhlman-Hahn","city":"New Ruthiehaven","progress":76,"created":"Sep 16, 2022","created_mm_dd_yyyy":"09-16-2022"},{"id":44,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Mr.-Watson-Brakus-PhD.svg","login":"oconnell.juanita","name":"Mr. Watson Brakus PhD","company":"Osinski, Bins and Kuhn","city":"Lake Gloria","progress":58,"created":"Jun 22, 2022","created_mm_dd_yyyy":"06-22-2022"},{"id":46,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Mr.-Garrison-Friesen-V.svg","login":"vgutmann","name":"Mr. Garrison Friesen V","company":"VonRueden, Rippin and Pfeffer","city":"Port Cieloport","progress":39,"created":"Oct 19, 2022","created_mm_dd_yyyy":"10-19-2022"},{"id":14,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Ms.-Sister-Morar.svg","login":"veum.lucio","name":"Ms. Sister Morar","company":"Gusikowski, Altenwerth and Abbott","city":"Lake Macville","progress":34,"created":"Jun 11, 2022","created_mm_dd_yyyy":"06-11-2022"},{"id":40,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Ms.-Laisha-Reinger.svg","login":"edietrich","name":"Ms. Laisha Reinger","company":"Boehm PLC","city":"West Alexiemouth","progress":73,"created":"Nov 2, 2022","created_mm_dd_yyyy":"11-02-2022"},{"id":5,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Cameron-Lind.svg","login":"mose44","name":"Cameron Lind","company":"Tremblay, Padberg and Pouros","city":"Naderview","progress":59,"created":"Sep 14, 2022","created_mm_dd_yyyy":"09-14-2022"},{"id":43,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Sarai-Little.svg","login":"rau.abelardo","name":"Sarai Little","company":"Deckow LLC","city":"Jeanieborough","progress":49,"created":"Jun 13, 2022","created_mm_dd_yyyy":"06-13-2022"},{"id":2,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Shyann-Kautzer.svg","login":"imurazik","name":"Shyann Kautzer","company":"Osinski, Boehm and Kihn","city":"New Alvera","progress":41,"created":"Feb 15, 2022","created_mm_dd_yyyy":"02-15-2022"},{"id":15,"avatar":"https:\/\/avatars.dicebear.com\/v2\/gridy\/Lorna-Christiansen.svg","login":"annalise97","name":"Lorna Christiansen","company":"Altenwerth-Friesen","city":"Port Elbertland","progress":36,"created":"Mar 9, 2022","created_mm_dd_yyyy":"03-09-2022"}]} diff --git a/frontend/public/data-sources/history.json b/frontend/public/data-sources/history.json new file mode 100644 index 0000000..1cc0c82 --- /dev/null +++ b/frontend/public/data-sources/history.json @@ -0,0 +1 @@ +{"data":[{"id":1,"amount":375.53,"account":"45721474","name":"Home Loan Account","date":"3 days ago","type":"deposit","business":"Turcotte"},{"id":2,"amount":470.26,"account":"94486537","name":"Savings Account","date":"3 days ago","type":"payment","business":"Murazik - Graham"},{"id":3,"amount":971.34,"account":"63189893","name":"Checking Account","date":"5 days ago","type":"invoice","business":"Fahey - Keebler"},{"id":4,"amount":374.63,"account":"74828780","name":"Auto Loan Account","date":"7 days ago","type":"withdraw","business":"Collier - Hintz"}]} \ No newline at end of file diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..c8c4e3e --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/colors.ts b/frontend/src/colors.ts new file mode 100644 index 0000000..ad1ff0e --- /dev/null +++ b/frontend/src/colors.ts @@ -0,0 +1,138 @@ +import type { ColorButtonKey } from './interfaces' + +export const gradientBgBase = 'bg-gradient-to-tr' +export const colorBgBase = 'bg-violet-50/50' +export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-red-500` +export const gradientBgViolet = `${gradientBgBase} ${colorBgBase}` +export const gradientBgDark = `${gradientBgBase} from-dark-700 via-dark-900 to-dark-800`; +export const gradientBgPinkRed = `${gradientBgBase} from-pink-400 via-red-500 to-yellow-500` + +export const colorsBgLight = { + white: 'bg-white text-black', + light: 'bg-white text-black text-black dark:bg-dark-900 dark:text-white', + contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', + success: 'bg-emerald-500 border-emerald-500 dark:bg-brand-blue dark:border-brand-blue text-white', + danger: 'bg-red-500 border-red-500 text-white', + warning: 'bg-yellow-500 border-yellow-500 text-white', + info: 'bg-blue-500 border-blue-500 dark:bg-brand-blue dark:border-brand-blue text-white', +} + +export const colorsText = { + white: 'text-black dark:text-slate-100', + light: 'text-gray-700 dark:text-slate-400', + contrast: 'dark:text-white', + success: 'text-emerald-500', + danger: 'text-red-500', + warning: 'text-yellow-500', + info: 'text-blue-500', +}; + +export const colorsOutline = { + white: [colorsText.white, 'border-gray-100'].join(' '), + light: [colorsText.light, 'border-gray-100'].join(' '), + contrast: [colorsText.contrast, 'border-gray-900 dark:border-slate-100'].join(' '), + success: [colorsText.success, 'border-emerald-500'].join(' '), + danger: [colorsText.danger, 'border-red-500'].join(' '), + warning: [colorsText.warning, 'border-yellow-500'].join(' '), + info: [colorsText.info, 'border-blue-500'].join(' '), +}; + +export const getButtonColor = ( + color: ColorButtonKey, + isOutlined: boolean, + hasHover: boolean, + isActive = false +) => { + if (color === 'void') { + return '' + } + + const colors = { + ring: { + white: 'ring-gray-200 dark:ring-gray-500', + whiteDark: 'ring-gray-200 dark:ring-dark-500', + lightDark: 'ring-gray-200 dark:ring-gray-500', + contrast: 'ring-gray-300 dark:ring-gray-400', + success: 'ring-emerald-300 dark:ring-brand-blue', + danger: 'ring-red-300 dark:ring-red-700', + warning: 'ring-yellow-300 dark:ring-yellow-700', + info: 'ring-blue-300 dark:ring-brand-blue', + }, + active: { + white: 'bg-gray-100', + whiteDark: 'bg-gray-100 dark:bg-dark-800', + lightDark: 'bg-gray-200 dark:bg-slate-700', + contrast: 'bg-gray-700 dark:bg-slate-100', + success: 'bg-emerald-700 dark:bg-brand-blue', + danger: 'bg-red-700 dark:bg-red-600', + warning: 'bg-yellow-700 dark:bg-yellow-600', + info: 'bg-blue-700 dark:bg-brand-blue', + }, + bg: { + white: 'bg-white text-black', + whiteDark: 'bg-white text-black dark:bg-dark-900 dark:text-white', + lightDark: 'bg-gray-100 text-black dark:bg-slate-800 dark:text-white', + contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', + success: 'bg-emerald-600 dark:bg-brand-blue text-white', + danger: 'bg-red-600 text-white dark:bg-red-500', + warning: 'bg-yellow-600 dark:bg-yellow-500 text-white', + info: 'bg-blue-600 dark:bg-brand-blue text-white', + }, + bgHover: { + white: 'hover:bg-gray-100', + whiteDark: 'hover:bg-gray-100 hover:dark:bg-dark-800', + lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700', + contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100', + success: + 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-brand-blue hover:dark:border-brand-blue', + danger: + 'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600', + warning: + 'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600', + info: 'hover:bg-blue-700 hover:border-blue-700 hover:dark:bg-brand-blue/80 hover:dark:border-brand-blue/80', + }, + borders: { + white: 'border-white', + whiteDark: 'border-white dark:border-dark-900', + lightDark: 'border-gray-100 dark:border-slate-800', + contrast: 'border-gray-800 dark:border-white', + success: 'border-emerald-600 dark:border-brand-blue', + danger: 'border-red-600 dark:border-red-500', + warning: 'border-yellow-600 dark:border-yellow-500', + info: 'border-blue-600 border-blue-600 dark:border-brand-blue', + }, + text: { + contrast: 'dark:text-slate-100', + success: 'text-emerald-600 dark:text-brand-blue', + danger: 'text-red-600 dark:text-red-500', + warning: 'text-yellow-600 dark:text-yellow-500', + info: 'text-blue-600 dark:text-brand-blue', + }, + outlineHover: { + contrast: + 'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black', + success: 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-brand-blue', + danger: + 'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600', + warning: + 'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600', + info: 'hover:bg-blue-600 hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-brand-blue', + }, + } + + const isOutlinedProcessed = isOutlined && ['white', 'whiteDark', 'lightDark'].indexOf(color) < 0 + + const base = [colors.borders[color], colors.ring[color]] + + if (isActive) { + base.push(colors.active[color]) + } else { + base.push(isOutlinedProcessed ? colors.text[color] : colors.bg[color]) + } + + if (hasHover) { + base.push(isOutlinedProcessed ? colors.outlineHover[color] : colors.bgHover[color]) + } + + return base.join(' ') +} diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx new file mode 100644 index 0000000..442dfac --- /dev/null +++ b/frontend/src/components/AsideMenu.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { MenuAsideItem } from '../interfaces' +import AsideMenuLayer from './AsideMenuLayer' +import OverlayLayer from './OverlayLayer' + +type Props = { + menu: MenuAsideItem[] + isAsideMobileExpanded: boolean + isAsideLgActive: boolean + onAsideLgClose: () => void +} + +export default function AsideMenu({ + isAsideMobileExpanded = false, + isAsideLgActive = false, + ...props +}: Props) { + return ( + <> + + {isAsideLgActive && } + + ) +} diff --git a/frontend/src/components/AsideMenuItem.tsx b/frontend/src/components/AsideMenuItem.tsx new file mode 100644 index 0000000..dbb09b2 --- /dev/null +++ b/frontend/src/components/AsideMenuItem.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from 'react' +import { mdiMinus, mdiPlus } from '@mdi/js' +import BaseIcon from './BaseIcon' +import Link from 'next/link' +import { getButtonColor } from '../colors' +import AsideMenuList from './AsideMenuList' +import { MenuAsideItem } from '../interfaces' +import { useAppSelector } from '../stores/hooks' +import { useRouter } from 'next/router' + +type Props = { + item: MenuAsideItem + isDropdownList?: boolean +} + +const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { + const [isLinkActive, setIsLinkActive] = useState(false) + const [isDropdownActive, setIsDropdownActive] = useState(false) + + const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle) + const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle) + const asideMenuItemActiveStyle = useAppSelector((state) => state.style.asideMenuItemActiveStyle) + const borders = useAppSelector((state) => state.style.borders); + const activeLinkColor = useAppSelector( + (state) => state.style.activeLinkColor, + ); + const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : '' + + const { asPath, isReady } = useRouter() + + useEffect(() => { + if (item.href && isReady) { + const linkPathName = new URL(item.href, location.href).pathname + '/'; + const activePathname = new URL(asPath, location.href).pathname + + const activeView = activePathname.split('/')[1]; + const linkPathNameView = linkPathName.split('/')[1]; + + setIsLinkActive(linkPathNameView === activeView); + } + }, [item.href, isReady, asPath]) + + const asideMenuItemInnerContents = ( + <> + {item.icon && ( + + )} + + {item.label} + + {item.menu && ( + + )} + + ) + + const componentClass = [ + 'flex cursor-pointer py-1.5 ', + isDropdownList ? 'px-6 text-sm' : '', + item.color + ? getButtonColor(item.color, false, true) + : `${asideMenuItemStyle}`, + isLinkActive + ? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800` + : '', + ].join(' '); + + return ( +
  • + {item.withDevider &&
    } + {item.href && ( + + {asideMenuItemInnerContents} + + )} + {!item.href && ( +
    setIsDropdownActive(!isDropdownActive)}> + {asideMenuItemInnerContents} +
    + )} + {item.menu && ( + + )} +
  • + ) +} + +export default AsideMenuItem diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx new file mode 100644 index 0000000..adff7f9 --- /dev/null +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { mdiClose } from '@mdi/js' +import BaseIcon from './BaseIcon' +import AsideMenuList from './AsideMenuList' +import { MenuAsideItem } from '../interfaces' +import { useAppSelector } from '../stores/hooks' +import Link from 'next/link'; +import { useAppDispatch } from '../stores/hooks'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; + +type Props = { + menu: MenuAsideItem[] + className?: string + onAsideLgCloseClick: () => void +} + +export default function AsideMenuLayer({ menu, className = '', ...props }: Props) { + const corners = useAppSelector((state) => state.style.corners); + const asideStyle = useAppSelector((state) => state.style.asideStyle) + const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle) + const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle) + const darkMode = useAppSelector((state) => state.style.darkMode) + + const handleAsideLgCloseClick = (e: React.MouseEvent) => { + e.preventDefault() + props.onAsideLgCloseClick() + } + + return ( + + ) +} diff --git a/frontend/src/components/AsideMenuList.tsx b/frontend/src/components/AsideMenuList.tsx new file mode 100644 index 0000000..1496110 --- /dev/null +++ b/frontend/src/components/AsideMenuList.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { MenuAsideItem } from '../interfaces' +import AsideMenuItem from './AsideMenuItem' +import {useAppSelector} from "../stores/hooks"; + +type Props = { + menu: MenuAsideItem[] + isDropdownList?: boolean + className?: string +} + +export default function AsideMenuList({ menu, isDropdownList = false, className = '' }: Props) { + const { currentUser } = useAppSelector((state) => state.auth); + + if (!currentUser) return null; + + return ( +
      + {menu.map((item, index) => { + return ( +
      + +
      + ) + })} +
    + ) +} diff --git a/frontend/src/components/BaseButton.tsx b/frontend/src/components/BaseButton.tsx new file mode 100644 index 0000000..a137dc2 --- /dev/null +++ b/frontend/src/components/BaseButton.tsx @@ -0,0 +1,96 @@ +import React from 'react' +import Link from 'next/link' +import { getButtonColor } from '../colors' +import BaseIcon from './BaseIcon' +import type { ColorButtonKey } from '../interfaces' +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string + icon?: string + iconSize?: string | number + href?: string + target?: string + type?: string + color?: ColorButtonKey + className?: string + iconClassName?: string + asAnchor?: boolean + small?: boolean + outline?: boolean + active?: boolean + disabled?: boolean + roundedFull?: boolean + onClick?: (e: React.MouseEvent) => void +} + +export default function BaseButton({ + label, + icon, + iconSize, + href, + target, + type, + color = 'white', + className = '', + iconClassName = '', + asAnchor = false, + small = false, + outline = false, + active = false, + disabled = false, + roundedFull = false, + onClick, +}: Props) { + const corners = useAppSelector((state) => state.style.corners); + const componentClass = [ + 'inline-flex', + 'justify-center', + 'items-center', + 'whitespace-nowrap', + 'focus:outline-none', + 'transition-colors', + 'focus:ring', + 'duration-150', + 'border', + disabled ? 'cursor-not-allowed' : 'cursor-pointer', + roundedFull ? 'rounded-full' : `${corners}`, + getButtonColor(color, outline, !disabled, active), + className, + ] + + if (!label && icon) { + componentClass.push('p-1') + } else if (small) { + componentClass.push('text-sm', roundedFull ? 'px-3 py-1' : 'p-1') + } else { + componentClass.push('py-2', roundedFull ? 'px-6' : 'px-3') + } + + if (disabled) { + componentClass.push(outline ? 'opacity-50' : 'opacity-70') + } + + const componentClassString = componentClass.join(' ') + + const componentChildren = ( + <> + {icon && } + {label && {label}} + + ) + + if (href && !disabled) { + return ( + + {componentChildren} + + ) + } + + return React.createElement( + asAnchor ? 'a' : 'button', + { className: componentClassString, type: type ?? 'button', target, disabled, onClick }, + componentChildren + ) +} diff --git a/frontend/src/components/BaseButtons.tsx b/frontend/src/components/BaseButtons.tsx new file mode 100644 index 0000000..e81a102 --- /dev/null +++ b/frontend/src/components/BaseButtons.tsx @@ -0,0 +1,38 @@ +import { Children, cloneElement, ReactElement } from 'react'; +import type { ReactNode } from 'react'; + +type Props = { + type?: string; + mb?: string; + noWrap?: boolean; + classAddon?: string; + children?: ReactNode; + className?: string; +}; + +const BaseButtons = ({ + type = 'justify-end', + mb = '-mb-3', + classAddon = 'mr-3 last:mr-0 mb-3', + noWrap = false, + children, + className, + }: Props) => { + return ( +
    + {Children.map(children, (child: ReactElement) => + child + ? cloneElement(child as ReactElement<{ className?: string }>, { + className: `${classAddon} ${(child.props as { className?: string }).className || ''}`, + }) + : null, + )} +
    + ); +}; + +export default BaseButtons; diff --git a/frontend/src/components/BaseDivider.tsx b/frontend/src/components/BaseDivider.tsx new file mode 100644 index 0000000..ac56fa6 --- /dev/null +++ b/frontend/src/components/BaseDivider.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { useAppSelector } from '../stores/hooks'; +type Props = { + navBar?: boolean +} + +export default function BaseDivider({ navBar = false }: Props) { + const borders = useAppSelector((state) => state.style.borders); + const classAddon = navBar + ? 'hidden lg:block lg:my-0.5 dark:border-dark-700' + : 'my-6 -mx-6 dark:border-dark-800' + + return
    +} diff --git a/frontend/src/components/BaseIcon.tsx b/frontend/src/components/BaseIcon.tsx new file mode 100644 index 0000000..98ec891 --- /dev/null +++ b/frontend/src/components/BaseIcon.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode } from 'react' + +type Props = { + path: string + w?: string + h?: string + fill?: string; + size?: string | number | null + className?: string + children?: ReactNode +} + +export default function BaseIcon({ + path, + fill, + w = 'w-6', + h = 'h-6', + size = null, + className = '', + children, +}: Props) { + const iconSize = size ?? 16 + + return ( + + + + + {children} + + ) +} diff --git a/frontend/src/components/Branches/CardBranches.tsx b/frontend/src/components/Branches/CardBranches.tsx new file mode 100644 index 0000000..83d0ba4 --- /dev/null +++ b/frontend/src/components/Branches/CardBranches.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + branches: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardBranches = ({ + branches, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && branches.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Code
    +
    +
    + { item.code } +
    +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    +
    Description
    +
    +
    + { item.description } +
    +
    +
    + +
    +
    Clients
    +
    +
    + { item.clients } +
    +
    +
    + +
    +
    Loans
    +
    +
    + { item.loans } +
    +
    +
    + +
    +
    Deposits
    +
    +
    + { item.deposits } +
    +
    +
    + +
    +
    ExpenseItems
    +
    +
    + { item.expense_items } +
    +
    +
    + +
    +
    PaymentRevenues
    +
    +
    + { item.payment_revenues } +
    +
    +
    + +
    +
    Staffs
    +
    +
    + { item.staffs } +
    +
    +
    + +
    + + ))} + {!loading && branches.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardBranches; diff --git a/frontend/src/components/Branches/ListBranches.tsx b/frontend/src/components/Branches/ListBranches.tsx new file mode 100644 index 0000000..1662a02 --- /dev/null +++ b/frontend/src/components/Branches/ListBranches.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + branches: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListBranches = ({ branches, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && branches.map((item) => ( +
    + +
    + + +
    +

    Code

    +

    { item.code }

    +
    + +
    +

    Name

    +

    { item.name }

    +
    + +
    +

    Description

    +

    { item.description }

    +
    + +
    +

    Clients

    +

    { item.clients }

    +
    + +
    +

    Loans

    +

    { item.loans }

    +
    + +
    +

    Deposits

    +

    { item.deposits }

    +
    + +
    +

    ExpenseItems

    +

    { item.expense_items }

    +
    + +
    +

    PaymentRevenues

    +

    { item.payment_revenues }

    +
    + +
    +

    Staffs

    +

    { item.staffs }

    +
    + + + +
    +
    +
    + ))} + {!loading && branches.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListBranches diff --git a/frontend/src/components/Branches/TableBranches.tsx b/frontend/src/components/Branches/TableBranches.tsx new file mode 100644 index 0000000..aff2b80 --- /dev/null +++ b/frontend/src/components/Branches/TableBranches.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/branches/branchesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureBranchesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleBranches = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { branches, loading, count, notify: branchesNotify, refetch } = useAppSelector((state) => state.branches) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (branchesNotify.showNotification) { + notify(branchesNotify.typeNotification, branchesNotify.textNotification); + } + }, [branchesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `branches`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={branches ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleBranches diff --git a/frontend/src/components/Branches/configureBranchesCols.tsx b/frontend/src/components/Branches/configureBranchesCols.tsx new file mode 100644 index 0000000..1701f0b --- /dev/null +++ b/frontend/src/components/Branches/configureBranchesCols.tsx @@ -0,0 +1,169 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'code', + headerName: 'Code', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'description', + headerName: 'Description', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'clients', + headerName: 'Clients', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'loans', + headerName: 'Loans', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'deposits', + headerName: 'Deposits', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'expense_items', + headerName: 'ExpenseItems', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'payment_revenues', + headerName: 'PaymentRevenues', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'staffs', + headerName: 'Staffs', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Calendars/CardCalendars.tsx b/frontend/src/components/Calendars/CardCalendars.tsx new file mode 100644 index 0000000..6aa4775 --- /dev/null +++ b/frontend/src/components/Calendars/CardCalendars.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + calendars: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardCalendars = ({ + calendars, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && calendars.map((item, index) => ( +
    • + + {item.description} + + +
      + +
      +
    +
    + +
    +
    Date
    +
    +
    + { dataFormatter.dateTimeFormatter(item.date) } +
    +
    +
    + +
    +
    IsWeekend
    +
    +
    + { dataFormatter.booleanFormatter(item.is_weekend) } +
    +
    +
    + +
    +
    IsHoliday
    +
    +
    + { dataFormatter.booleanFormatter(item.is_holiday) } +
    +
    +
    + +
    +
    Description
    +
    +
    + { item.description } +
    +
    +
    + +
    +
    Flag
    +
    +
    + { item.flag } +
    +
    +
    + +
    + + ))} + {!loading && calendars.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardCalendars; diff --git a/frontend/src/components/Calendars/ListCalendars.tsx b/frontend/src/components/Calendars/ListCalendars.tsx new file mode 100644 index 0000000..fdb2d02 --- /dev/null +++ b/frontend/src/components/Calendars/ListCalendars.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + calendars: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListCalendars = ({ calendars, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && calendars.map((item) => ( +
    + +
    + + +
    +

    Date

    +

    { dataFormatter.dateTimeFormatter(item.date) }

    +
    + +
    +

    IsWeekend

    +

    { dataFormatter.booleanFormatter(item.is_weekend) }

    +
    + +
    +

    IsHoliday

    +

    { dataFormatter.booleanFormatter(item.is_holiday) }

    +
    + +
    +

    Description

    +

    { item.description }

    +
    + +
    +

    Flag

    +

    { item.flag }

    +
    + + + +
    +
    +
    + ))} + {!loading && calendars.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListCalendars diff --git a/frontend/src/components/Calendars/TableCalendars.tsx b/frontend/src/components/Calendars/TableCalendars.tsx new file mode 100644 index 0000000..8a5243d --- /dev/null +++ b/frontend/src/components/Calendars/TableCalendars.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/calendars/calendarsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureCalendarsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleCalendars = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { calendars, loading, count, notify: calendarsNotify, refetch } = useAppSelector((state) => state.calendars) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (calendarsNotify.showNotification) { + notify(calendarsNotify.typeNotification, calendarsNotify.textNotification); + } + }, [calendarsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `calendars`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={calendars ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleCalendars diff --git a/frontend/src/components/Calendars/configureCalendarsCols.tsx b/frontend/src/components/Calendars/configureCalendarsCols.tsx new file mode 100644 index 0000000..3f14c7f --- /dev/null +++ b/frontend/src/components/Calendars/configureCalendarsCols.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'date', + headerName: 'Date', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.date), + + }, + + { + field: 'is_weekend', + headerName: 'IsWeekend', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'is_holiday', + headerName: 'IsHoliday', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'description', + headerName: 'Description', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'flag', + headerName: 'Flag', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/CardBox.tsx b/frontend/src/components/CardBox.tsx new file mode 100644 index 0000000..900386a --- /dev/null +++ b/frontend/src/components/CardBox.tsx @@ -0,0 +1,64 @@ +import React, { ReactNode } from 'react' +import CardBoxComponentBody from './CardBoxComponentBody' +import CardBoxComponentFooter from './CardBoxComponentFooter' +import { useAppSelector } from '../stores/hooks'; + +type Props = { + rounded?: string + flex?: string + className?: string + hasComponentLayout?: boolean + cardBoxClassName?: string + hasTable?: boolean + isHoverable?: boolean + isModal?: boolean + children?: ReactNode + footer?: ReactNode + isList?:boolean + id?: string; + onClick?: (e: React.MouseEvent) => void +} + +export default function CardBox({ + rounded = 'rounded', + flex = 'flex-col', + className = '', + hasComponentLayout = false, + cardBoxClassName = '', + hasTable = false, + isHoverable = false, + isList = false, + isModal = false, + children, + footer, + id ='', + onClick, +}: Props) { + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const componentClass = [ + `flex dark:border-dark-700 dark:bg-dark-900`, + className, + corners !== 'rounded-full'? corners : 'rounded-3xl', + flex, + isList ? '' : `${cardsStyle}`, + hasTable ? '' : `border border-brand-400 dark:border-dark-700` + ] + + if (isHoverable) { + componentClass.push('hover:shadow-lg transition-shadow duration-500') + } + + return React.createElement( + 'div', + { className: componentClass.join(' '), onClick }, + hasComponentLayout ? ( + children + ) : ( + <> + {children} + {footer && {footer}} + + ) + ) +} diff --git a/frontend/src/components/CardBoxComponentBody.tsx b/frontend/src/components/CardBoxComponentBody.tsx new file mode 100644 index 0000000..4c89896 --- /dev/null +++ b/frontend/src/components/CardBoxComponentBody.tsx @@ -0,0 +1,12 @@ +import React, { ReactNode } from 'react' + +type Props = { + noPadding?: boolean + className?: string + children?: ReactNode + id?: string +} + +export default function CardBoxComponentBody({ noPadding = false, className, children, id }: Props) { + return
    {children}
    +} diff --git a/frontend/src/components/CardBoxComponentEmpty.tsx b/frontend/src/components/CardBoxComponentEmpty.tsx new file mode 100644 index 0000000..c71413e --- /dev/null +++ b/frontend/src/components/CardBoxComponentEmpty.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +const CardBoxComponentEmpty = () => { + return ( +
    +

    Nothing's here…

    +
    + ) +} + +export default CardBoxComponentEmpty diff --git a/frontend/src/components/CardBoxComponentFooter.tsx b/frontend/src/components/CardBoxComponentFooter.tsx new file mode 100644 index 0000000..4b41bba --- /dev/null +++ b/frontend/src/components/CardBoxComponentFooter.tsx @@ -0,0 +1,10 @@ +import React, { ReactNode } from 'react' + +type Props = { + className?: string + children?: ReactNode +} + +export default function CardBoxComponentFooter({ className, children }: Props) { + return
    {children}
    +} diff --git a/frontend/src/components/CardBoxComponentTitle.tsx b/frontend/src/components/CardBoxComponentTitle.tsx new file mode 100644 index 0000000..4f92def --- /dev/null +++ b/frontend/src/components/CardBoxComponentTitle.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react' + +type Props = { + title: string + children?: ReactNode +} + +const CardBoxComponentTitle = ({ title, children }: Props) => { + return ( +
    +

    {title}

    + {children} +
    + ) +} + +export default CardBoxComponentTitle diff --git a/frontend/src/components/CardBoxModal.tsx b/frontend/src/components/CardBoxModal.tsx new file mode 100644 index 0000000..27d5676 --- /dev/null +++ b/frontend/src/components/CardBoxModal.tsx @@ -0,0 +1,59 @@ +import { mdiClose } from '@mdi/js' +import { ReactNode } from 'react' +import type { ColorButtonKey } from '../interfaces' +import BaseButton from './BaseButton' +import BaseButtons from './BaseButtons' +import CardBox from './CardBox' +import CardBoxComponentTitle from './CardBoxComponentTitle' +import OverlayLayer from './OverlayLayer' + +type Props = { + title: string + buttonColor: ColorButtonKey + buttonLabel: string + isActive: boolean + children?: ReactNode + onConfirm: () => void + onCancel?: () => void +} + +const CardBoxModal = ({ + title, + buttonColor, + buttonLabel, + isActive, + children, + onConfirm, + onCancel, +}: Props) => { + if (!isActive) { + return null + } + + const footer = ( + + + {!!onCancel && } + + ) + + return ( + + + + {!!onCancel && ( + + )} + + +
    {children}
    +
    +
    + ) +} + +export default CardBoxModal diff --git a/frontend/src/components/ChartLineSample/config.ts b/frontend/src/components/ChartLineSample/config.ts new file mode 100644 index 0000000..cacce92 --- /dev/null +++ b/frontend/src/components/ChartLineSample/config.ts @@ -0,0 +1,54 @@ +export const chartColors = { + default: { + primary: '#00D1B2', + info: '#209CEE', + danger: '#FF3860', + }, +} + +const randomChartData = (n: number) => { + const data = [] + + for (let i = 0; i < n; i++) { + data.push(Math.round(Math.random() * 200)) + } + + return data +} + +const datasetObject = (color: string, points: number) => { + return { + fill: false, + borderColor: chartColors.default[color], + borderWidth: 2, + borderDash: [], + borderDashOffset: 0.0, + pointBackgroundColor: chartColors.default[color], + pointBorderColor: 'rgba(255,255,255,0)', + pointHoverBackgroundColor: chartColors.default[color], + pointBorderWidth: 20, + pointHoverRadius: 4, + pointHoverBorderWidth: 15, + pointRadius: 4, + data: randomChartData(points), + tension: 0.5, + cubicInterpolationMode: 'default', + } +} + +export const sampleChartData = (points = 9) => { + const labels = [] + + for (let i = 1; i <= points; i++) { + labels.push(`0${i}`) + } + + return { + labels, + datasets: [ + datasetObject('primary', points), + datasetObject('info', points), + datasetObject('danger', points), + ], + } +} diff --git a/frontend/src/components/ChartLineSample/index.tsx b/frontend/src/components/ChartLineSample/index.tsx new file mode 100644 index 0000000..c7f417b --- /dev/null +++ b/frontend/src/components/ChartLineSample/index.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { + Chart, + LineElement, + PointElement, + LineController, + LinearScale, + CategoryScale, + Tooltip, +} from 'chart.js' +import { Line } from 'react-chartjs-2' + +Chart.register(LineElement, PointElement, LineController, LinearScale, CategoryScale, Tooltip) + +const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + display: false, + }, + x: { + display: true, + }, + }, + plugins: { + legend: { + display: false, + }, + }, +} + +const ChartLineSample = ({ data }) => { + return +} + +export default ChartLineSample diff --git a/frontend/src/components/ClickOutside.tsx b/frontend/src/components/ClickOutside.tsx new file mode 100644 index 0000000..1673e29 --- /dev/null +++ b/frontend/src/components/ClickOutside.tsx @@ -0,0 +1,35 @@ +import React, { useCallback, useEffect, useRef, ReactNode, MutableRefObject } from 'react'; + +interface ClickOutsideProps { + children?: ReactNode; + onClickOutside: () => void; + excludedElements: MutableRefObject[]; +} + +const ClickOutside = ({ children, onClickOutside, excludedElements }: ClickOutsideProps) => { + const wrapperRef = useRef(null); + + const handleClickOutside = useCallback( + (event) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target) && + !excludedElements.some((el) => el.current.contains(event.target)) + ) { + onClickOutside(); + } + }, + [wrapperRef, onClickOutside, ...excludedElements], + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); + + return
    {children}
    ; +}; + +export default ClickOutside; diff --git a/frontend/src/components/Client_status/CardClient_status.tsx b/frontend/src/components/Client_status/CardClient_status.tsx new file mode 100644 index 0000000..7524e32 --- /dev/null +++ b/frontend/src/components/Client_status/CardClient_status.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + client_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardClient_status = ({ + client_status, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && client_status.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    + + ))} + {!loading && client_status.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardClient_status; diff --git a/frontend/src/components/Client_status/ListClient_status.tsx b/frontend/src/components/Client_status/ListClient_status.tsx new file mode 100644 index 0000000..ccd34c1 --- /dev/null +++ b/frontend/src/components/Client_status/ListClient_status.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + client_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListClient_status = ({ client_status, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && client_status.map((item) => ( +
    + +
    + + +
    +

    Name

    +

    { item.name }

    +
    + + + +
    +
    +
    + ))} + {!loading && client_status.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListClient_status diff --git a/frontend/src/components/Client_status/TableClient_status.tsx b/frontend/src/components/Client_status/TableClient_status.tsx new file mode 100644 index 0000000..24e2ef0 --- /dev/null +++ b/frontend/src/components/Client_status/TableClient_status.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/client_status/client_statusSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureClient_statusCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleClient_status = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { client_status, loading, count, notify: client_statusNotify, refetch } = useAppSelector((state) => state.client_status) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (client_statusNotify.showNotification) { + notify(client_statusNotify.typeNotification, client_statusNotify.textNotification); + } + }, [client_statusNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `client_status`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={client_status ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleClient_status diff --git a/frontend/src/components/Client_status/configureClient_statusCols.tsx b/frontend/src/components/Client_status/configureClient_statusCols.tsx new file mode 100644 index 0000000..f4a2de2 --- /dev/null +++ b/frontend/src/components/Client_status/configureClient_statusCols.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Clients/CardClients.tsx b/frontend/src/components/Clients/CardClients.tsx new file mode 100644 index 0000000..e47e7dd --- /dev/null +++ b/frontend/src/components/Clients/CardClients.tsx @@ -0,0 +1,228 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + clients: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardClients = ({ + clients, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && clients.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    Code
    +
    +
    + { item.code } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    DateofBirth
    +
    +
    + { dataFormatter.dateTimeFormatter(item.date_of_birth) } +
    +
    +
    + +
    +
    PhoneNumber
    +
    +
    + { item.phone_number } +
    +
    +
    + +
    +
    IsNew
    +
    +
    + { dataFormatter.booleanFormatter(item.is_new) } +
    +
    +
    + +
    +
    DocumentType
    +
    +
    + { dataFormatter.document_typeOneListFormatter(item.document_type) } +
    +
    +
    + +
    +
    DocumentNumber
    +
    +
    + { item.document_number } +
    +
    +
    + +
    +
    Sex
    +
    +
    + { item.sex } +
    +
    +
    + +
    +
    Status
    +
    +
    + { dataFormatter.client_statusOneListFormatter(item.status) } +
    +
    +
    + +
    +
    User
    +
    +
    + { dataFormatter.usersOneListFormatter(item.user) } +
    +
    +
    + +
    +
    Province
    +
    +
    + { dataFormatter.provincesOneListFormatter(item.province) } +
    +
    +
    + +
    +
    District
    +
    +
    + { dataFormatter.districtsOneListFormatter(item.district) } +
    +
    +
    + +
    +
    Commune
    +
    +
    + { dataFormatter.communesOneListFormatter(item.commune) } +
    +
    +
    + +
    +
    Village
    +
    +
    + { dataFormatter.villagesOneListFormatter(item.village) } +
    +
    +
    + +
    +
    Branch
    +
    +
    + { dataFormatter.branchesOneListFormatter(item.branch) } +
    +
    +
    + +
    + + ))} + {!loading && clients.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardClients; diff --git a/frontend/src/components/Clients/ListClients.tsx b/frontend/src/components/Clients/ListClients.tsx new file mode 100644 index 0000000..27a44ef --- /dev/null +++ b/frontend/src/components/Clients/ListClients.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + clients: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListClients = ({ clients, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && clients.map((item) => ( +
    + +
    + + +
    +

    Code

    +

    { item.code }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    DateofBirth

    +

    { dataFormatter.dateTimeFormatter(item.date_of_birth) }

    +
    + +
    +

    PhoneNumber

    +

    { item.phone_number }

    +
    + +
    +

    IsNew

    +

    { dataFormatter.booleanFormatter(item.is_new) }

    +
    + +
    +

    DocumentType

    +

    { dataFormatter.document_typeOneListFormatter(item.document_type) }

    +
    + +
    +

    DocumentNumber

    +

    { item.document_number }

    +
    + +
    +

    Sex

    +

    { item.sex }

    +
    + +
    +

    Status

    +

    { dataFormatter.client_statusOneListFormatter(item.status) }

    +
    + +
    +

    User

    +

    { dataFormatter.usersOneListFormatter(item.user) }

    +
    + +
    +

    Province

    +

    { dataFormatter.provincesOneListFormatter(item.province) }

    +
    + +
    +

    District

    +

    { dataFormatter.districtsOneListFormatter(item.district) }

    +
    + +
    +

    Commune

    +

    { dataFormatter.communesOneListFormatter(item.commune) }

    +
    + +
    +

    Village

    +

    { dataFormatter.villagesOneListFormatter(item.village) }

    +
    + +
    +

    Branch

    +

    { dataFormatter.branchesOneListFormatter(item.branch) }

    +
    + + + +
    +
    +
    + ))} + {!loading && clients.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListClients diff --git a/frontend/src/components/Clients/TableClients.tsx b/frontend/src/components/Clients/TableClients.tsx new file mode 100644 index 0000000..a214025 --- /dev/null +++ b/frontend/src/components/Clients/TableClients.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/clients/clientsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureClientsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleClients = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { clients, loading, count, notify: clientsNotify, refetch } = useAppSelector((state) => state.clients) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (clientsNotify.showNotification) { + notify(clientsNotify.typeNotification, clientsNotify.textNotification); + } + }, [clientsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `clients`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={clients ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleClients diff --git a/frontend/src/components/Clients/configureClientsCols.tsx b/frontend/src/components/Clients/configureClientsCols.tsx new file mode 100644 index 0000000..d9efd5e --- /dev/null +++ b/frontend/src/components/Clients/configureClientsCols.tsx @@ -0,0 +1,320 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'code', + headerName: 'Code', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'date_of_birth', + headerName: 'DateofBirth', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.date_of_birth), + + }, + + { + field: 'phone_number', + headerName: 'PhoneNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'is_new', + headerName: 'IsNew', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'document_type', + headerName: 'DocumentType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('document_type'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'document_number', + headerName: 'DocumentNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'sex', + headerName: 'Sex', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('client_status'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'province', + headerName: 'Province', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('provinces'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'district', + headerName: 'District', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('districts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'commune', + headerName: 'Commune', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('communes'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'village', + headerName: 'Village', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('villages'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'branch', + headerName: 'Branch', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('branches'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Communes/CardCommunes.tsx b/frontend/src/components/Communes/CardCommunes.tsx new file mode 100644 index 0000000..bdce9f7 --- /dev/null +++ b/frontend/src/components/Communes/CardCommunes.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + communes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardCommunes = ({ + communes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && communes.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    District
    +
    +
    + { dataFormatter.districtsOneListFormatter(item.district) } +
    +
    +
    + +
    + + ))} + {!loading && communes.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardCommunes; diff --git a/frontend/src/components/Communes/ListCommunes.tsx b/frontend/src/components/Communes/ListCommunes.tsx new file mode 100644 index 0000000..f954a89 --- /dev/null +++ b/frontend/src/components/Communes/ListCommunes.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + communes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListCommunes = ({ communes, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && communes.map((item) => ( +
    + +
    + + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    District

    +

    { dataFormatter.districtsOneListFormatter(item.district) }

    +
    + + + +
    +
    +
    + ))} + {!loading && communes.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListCommunes diff --git a/frontend/src/components/Communes/TableCommunes.tsx b/frontend/src/components/Communes/TableCommunes.tsx new file mode 100644 index 0000000..fb55103 --- /dev/null +++ b/frontend/src/components/Communes/TableCommunes.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/communes/communesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureCommunesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleCommunes = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { communes, loading, count, notify: communesNotify, refetch } = useAppSelector((state) => state.communes) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (communesNotify.showNotification) { + notify(communesNotify.typeNotification, communesNotify.textNotification); + } + }, [communesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `communes`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={communes ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleCommunes diff --git a/frontend/src/components/Communes/configureCommunesCols.tsx b/frontend/src/components/Communes/configureCommunesCols.tsx new file mode 100644 index 0000000..d833f7d --- /dev/null +++ b/frontend/src/components/Communes/configureCommunesCols.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'district', + headerName: 'District', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('districts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/DataGridMultiSelect.tsx b/frontend/src/components/DataGridMultiSelect.tsx new file mode 100644 index 0000000..bb82434 --- /dev/null +++ b/frontend/src/components/DataGridMultiSelect.tsx @@ -0,0 +1,55 @@ +import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid'; +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { MenuItem, Select } from '@mui/material'; + +interface Props { + entityName: string; +} + +const DataGridMultiSelect = (props: GridRenderEditCellParams & Props) => { + const { id, value, field, entityName } = props; + const apiRef = useGridApiContext(); + const [options, setOptions] = useState([]); + + async function callApi(entityName: string) { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } + + useEffect(() => { + callApi(entityName).then((data) => { + setOptions(data); + }); + }, []); + + const handleChange = (event) => { + const eventValue = event.target.value; // The new value entered by the user + + const newValue = + typeof eventValue === 'string' ? value.split(',') : eventValue; + + apiRef.current.setEditCellValue({ + id, + field, + value: newValue.filter((x) => x !== ''), + }); + }; + + return ( + + ); +}; + +export default DataGridMultiSelect; diff --git a/frontend/src/components/Deposits/CardDeposits.tsx b/frontend/src/components/Deposits/CardDeposits.tsx new file mode 100644 index 0000000..0b809fb --- /dev/null +++ b/frontend/src/components/Deposits/CardDeposits.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + deposits: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardDeposits = ({ + deposits, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && deposits.map((item, index) => ( +
    • + + {item.deposit_amount} + + +
      + +
      +
    +
    + +
    +
    DepositDatetime
    +
    +
    + { dataFormatter.dateTimeFormatter(item.deposit_datetime) } +
    +
    +
    + +
    +
    DepositAmount
    +
    +
    + { item.deposit_amount } +
    +
    +
    + +
    +
    Shareholder
    +
    +
    + { dataFormatter.shareholdersOneListFormatter(item.shareholder) } +
    +
    +
    + +
    +
    Branch
    +
    +
    + { dataFormatter.branchesOneListFormatter(item.branch) } +
    +
    +
    + +
    + + ))} + {!loading && deposits.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardDeposits; diff --git a/frontend/src/components/Deposits/ListDeposits.tsx b/frontend/src/components/Deposits/ListDeposits.tsx new file mode 100644 index 0000000..6492ff8 --- /dev/null +++ b/frontend/src/components/Deposits/ListDeposits.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + deposits: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListDeposits = ({ deposits, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && deposits.map((item) => ( +
    + +
    + + +
    +

    DepositDatetime

    +

    { dataFormatter.dateTimeFormatter(item.deposit_datetime) }

    +
    + +
    +

    DepositAmount

    +

    { item.deposit_amount }

    +
    + +
    +

    Shareholder

    +

    { dataFormatter.shareholdersOneListFormatter(item.shareholder) }

    +
    + +
    +

    Branch

    +

    { dataFormatter.branchesOneListFormatter(item.branch) }

    +
    + + + +
    +
    +
    + ))} + {!loading && deposits.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListDeposits diff --git a/frontend/src/components/Deposits/TableDeposits.tsx b/frontend/src/components/Deposits/TableDeposits.tsx new file mode 100644 index 0000000..cd4dddf --- /dev/null +++ b/frontend/src/components/Deposits/TableDeposits.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/deposits/depositsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureDepositsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleDeposits = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { deposits, loading, count, notify: depositsNotify, refetch } = useAppSelector((state) => state.deposits) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (depositsNotify.showNotification) { + notify(depositsNotify.typeNotification, depositsNotify.textNotification); + } + }, [depositsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `deposits`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={deposits ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleDeposits diff --git a/frontend/src/components/Deposits/configureDepositsCols.tsx b/frontend/src/components/Deposits/configureDepositsCols.tsx new file mode 100644 index 0000000..818d2c1 --- /dev/null +++ b/frontend/src/components/Deposits/configureDepositsCols.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'deposit_datetime', + headerName: 'DepositDatetime', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.deposit_datetime), + + }, + + { + field: 'deposit_amount', + headerName: 'DepositAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'shareholder', + headerName: 'Shareholder', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('shareholders'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'branch', + headerName: 'Branch', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('branches'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/DevModeBadge.tsx b/frontend/src/components/DevModeBadge.tsx new file mode 100644 index 0000000..bc238c4 --- /dev/null +++ b/frontend/src/components/DevModeBadge.tsx @@ -0,0 +1,150 @@ +import React, { useState, useEffect } from 'react'; +import useDevCompilationStatus from '../hooks/useDevCompilationStatus'; +const DevModeBadge: React.FC = () => { + const [isVisible, setIsVisible] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(true); + const compilationStatus = useDevCompilationStatus(); + + const [badgeStyles, setBadgeStyles] = useState({ + position: 'fixed', + bottom: '20px', + left: '70px', + background: 'rgba(0, 0, 0, 0.85)', + color: 'white', + padding: '15px', + borderRadius: '8px', + fontFamily: 'sans-serif', + fontSize: '14px', + lineHeight: '1.5', + textAlign: 'left', + zIndex: 2147483647, + boxShadow: '0 4px 10px rgba(0, 0, 0, 0.3)', + whiteSpace: 'pre-wrap', + transition: 'width 0.3s cubic-bezier(0.25, 0.1, 0.25, 1), padding 0.3s ease-in-out, opacity 0.3s ease-in-out, background-color 0.3s ease-in-out', // Improved transition for width + opacity: 0, + pointerEvents: 'none', + width: '340px', + maxWidth: '340px', + height: 'auto', + overflow: 'hidden', + cursor: 'pointer', + }); + + const fullText = `🚧 Your app is running in development mode. +Current request is compiling and may take a few moments. + +💡 Tip: Set up a stable environment to run your app in production mode—pages will load instantly without compilation delays.`; + + const collapsedText = '🚧 DEV stage'; + + useEffect(() => { + if (compilationStatus === 'ready') { + setIsCollapsed(true); + } else { + setIsCollapsed(false); + } + + }, [compilationStatus]); + + useEffect(() => { + if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') { + setIsVisible(true); + + setBadgeStyles(prev => ({ + ...prev, + opacity: 1, + width: '120px', + maxWidth: '120px', + padding: '6px 10px', + borderRadius: '18px', + whiteSpace: 'nowrap', + fontSize: '12px', + cursor: 'pointer', + pointerEvents: 'auto', + })); + + } else { + setIsVisible(false); + setBadgeStyles(prev => ({ ...prev, opacity: 0 })); + } + }, []); + + useEffect(() => { + if (!isVisible) return; + + if (isCollapsed) { + setBadgeStyles(prev => ({ + ...prev, + width: '140px', + maxWidth: '160px', + padding: '6px 20px', + borderRadius: '18px', + whiteSpace: 'nowrap', + fontSize: '12px', + })); + } else { + setBadgeStyles(prev => ({ + ...prev, + width: '340px', + maxWidth: '340px', + padding: '15px', + borderRadius: '8px', + whiteSpace: 'pre-wrap', + fontSize: '14px', + })); + } + }, [isCollapsed, isVisible]); + + const handleToggleCollapse = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsCollapsed(prev => !prev); + }; + + if (!isVisible) { + return null; + } + + return ( +
    + + + {!isCollapsed && ( +
    + {fullText} +
    + )} + {isCollapsed && ( +
    + {collapsedText} +
    + )} +
    + ); +}; + +export default DevModeBadge; diff --git a/frontend/src/components/Districts/CardDistricts.tsx b/frontend/src/components/Districts/CardDistricts.tsx new file mode 100644 index 0000000..07b52da --- /dev/null +++ b/frontend/src/components/Districts/CardDistricts.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + districts: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardDistricts = ({ + districts, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && districts.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    Province
    +
    +
    + { dataFormatter.provincesOneListFormatter(item.province) } +
    +
    +
    + +
    + + ))} + {!loading && districts.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardDistricts; diff --git a/frontend/src/components/Districts/ListDistricts.tsx b/frontend/src/components/Districts/ListDistricts.tsx new file mode 100644 index 0000000..79ff821 --- /dev/null +++ b/frontend/src/components/Districts/ListDistricts.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + districts: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListDistricts = ({ districts, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && districts.map((item) => ( +
    + +
    + + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    Province

    +

    { dataFormatter.provincesOneListFormatter(item.province) }

    +
    + + + +
    +
    +
    + ))} + {!loading && districts.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListDistricts diff --git a/frontend/src/components/Districts/TableDistricts.tsx b/frontend/src/components/Districts/TableDistricts.tsx new file mode 100644 index 0000000..2952609 --- /dev/null +++ b/frontend/src/components/Districts/TableDistricts.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/districts/districtsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureDistrictsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleDistricts = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { districts, loading, count, notify: districtsNotify, refetch } = useAppSelector((state) => state.districts) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (districtsNotify.showNotification) { + notify(districtsNotify.typeNotification, districtsNotify.textNotification); + } + }, [districtsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `districts`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={districts ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleDistricts diff --git a/frontend/src/components/Districts/configureDistrictsCols.tsx b/frontend/src/components/Districts/configureDistrictsCols.tsx new file mode 100644 index 0000000..a8be590 --- /dev/null +++ b/frontend/src/components/Districts/configureDistrictsCols.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'province', + headerName: 'Province', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('provinces'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Document_type/CardDocument_type.tsx b/frontend/src/components/Document_type/CardDocument_type.tsx new file mode 100644 index 0000000..7df3df3 --- /dev/null +++ b/frontend/src/components/Document_type/CardDocument_type.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + document_type: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardDocument_type = ({ + document_type, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && document_type.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    + + ))} + {!loading && document_type.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardDocument_type; diff --git a/frontend/src/components/Document_type/ListDocument_type.tsx b/frontend/src/components/Document_type/ListDocument_type.tsx new file mode 100644 index 0000000..f3e077e --- /dev/null +++ b/frontend/src/components/Document_type/ListDocument_type.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + document_type: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListDocument_type = ({ document_type, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && document_type.map((item) => ( +
    + +
    + + +
    +

    Name

    +

    { item.name }

    +
    + + + +
    +
    +
    + ))} + {!loading && document_type.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListDocument_type diff --git a/frontend/src/components/Document_type/TableDocument_type.tsx b/frontend/src/components/Document_type/TableDocument_type.tsx new file mode 100644 index 0000000..a1c476e --- /dev/null +++ b/frontend/src/components/Document_type/TableDocument_type.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/document_type/document_typeSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureDocument_typeCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleDocument_type = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { document_type, loading, count, notify: document_typeNotify, refetch } = useAppSelector((state) => state.document_type) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (document_typeNotify.showNotification) { + notify(document_typeNotify.typeNotification, document_typeNotify.textNotification); + } + }, [document_typeNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `document_type`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={document_type ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleDocument_type diff --git a/frontend/src/components/Document_type/configureDocument_typeCols.tsx b/frontend/src/components/Document_type/configureDocument_typeCols.tsx new file mode 100644 index 0000000..0b81bec --- /dev/null +++ b/frontend/src/components/Document_type/configureDocument_typeCols.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/DragDropFilePicker.tsx b/frontend/src/components/DragDropFilePicker.tsx new file mode 100644 index 0000000..f6d0ae4 --- /dev/null +++ b/frontend/src/components/DragDropFilePicker.tsx @@ -0,0 +1,124 @@ +import React, { ChangeEvent, useEffect, useState } from 'react'; +import BaseIcon from './BaseIcon'; +import { mdiFileUploadOutline } from '@mdi/js'; + +type Props = { + file: File | null; + setFile: (file: File) => void; + formats?: string; +}; + +const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { + const [highlight, setHighlight] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const fileInput = React.createRef(); + + useEffect(() => { + if (!file && fileInput) fileInput.current.value = ''; + }, [file, fileInput]); + + function onFilesAdded(files: FileList | null) { + if (files && files[0]) { + const newFile = files[0]; + const fileExtension = newFile.name.split('.').pop().toLowerCase(); + + if (formats.includes(fileExtension) || !formats) { + setFile(newFile); + setErrorMessage(''); + } else { + setErrorMessage(`Allowed formats: ${formats}`); + } + } + } + + function onDragOver(e) { + e.preventDefault(); + setHighlight(true); + } + + function onDragLeave() { + setHighlight(false); + } + + function onDrop(e) { + e.preventDefault(); + + const files = e.dataTransfer.files; + + onFilesAdded(files); + setHighlight(false); + } + + const onClear = () => { + setFile(null); + setErrorMessage(''); + }; + + return ( +
    + +
    + ); +}; + +export default DragDropFilePicker; diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..e889c09 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,218 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { mdiAlertCircle } from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +// Define the props and state interfaces +interface ErrorBoundaryProps { + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; + showStack: boolean; +} + +// Class-based ErrorBoundary Component +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + + // Define state variables + this.state = { + hasError: false, + error: null, + errorInfo: null, + showStack: false, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + // Update state so the next render will show the fallback UI + return { + hasError: true, + error: error, + }; + } + + componentDidUpdate( + prevProps: Readonly, + prevState: Readonly, + snapshot?: any, + ) { + if (process.env.NODE_ENV !== 'production') { + console.log('componentDidUpdate'); + } + } + + async componentWillUnmount() { + if (process.env.NODE_ENV !== 'production') { + console.log('componentWillUnmount'); + const response = await fetch('/api/logError', { + method: 'DELETE', + }); + + const data = await response.json(); + console.log('Error logs cleared:', data); + } + } + + async componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Update state with error details (always needed for UI) + this.setState({ + errorInfo: errorInfo, + }); + + // Only perform logging in non-production environments + if (process.env.NODE_ENV !== 'production') { + console.log('Error caught in boundary:', error, errorInfo); + + // Function to log errors to the server + const logErrorToServer = async () => { + try { + const response = await fetch('/api/logError', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: error.message, + stack: errorInfo.componentStack, + }), + }); + + const data = await response.json(); + console.log('Error logged:', data); + } catch (err) { + console.error('Failed to log error:', err); + } + }; + + // Function to fetch logged errors (optional) + const fetchLoggedErrors = async () => { + try { + const response = await fetch('/api/logError'); + const data = await response.json(); + console.log('Fetched logs:', data); + } catch (err) { + console.error('Failed to fetch logs:', err); + } + }; + + await logErrorToServer(); + await fetchLoggedErrors(); + } + } + + toggleStack = () => { + this.setState((prevState) => ({ + showStack: !prevState.showStack, + })); + }; + + resetError = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + showStack: false, + }); + }; + + tryAgain = async () => { + // Only clear error logs in non-production environments + if (process.env.NODE_ENV !== 'production') { + try { + const response = await fetch('/api/logError', { + method: 'DELETE', + }); + + const data = await response.json(); + console.log('Error logs cleared:', data); + } catch (e) { + console.error('Failed to clear error logs:', e); + } + } + + // Always reset the error state (needed for UI recovery) + this.setState({ hasError: false }); + }; + + render() { + if (this.state.hasError) { + // Extract error details + const { error, errorInfo, showStack } = this.state; + const errorMessage = error?.message || 'An unexpected error occurred'; + const stackTrace = + errorInfo?.componentStack || error?.stack || 'No stack trace available'; + + return ( +
    +
    +
    +
    + +
    + +
    +

    + Something went wrong +

    +

    + We're sorry, but we encountered an unexpected error. +

    +
    + +
    +

    + {errorMessage} +

    + +
    + + + {showStack && ( +
    +                      {stackTrace}
    +                    
    + )} +
    +
    + +
    + + + +
    +
    +
    +
    + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/Expense_items/CardExpense_items.tsx b/frontend/src/components/Expense_items/CardExpense_items.tsx new file mode 100644 index 0000000..f3d909f --- /dev/null +++ b/frontend/src/components/Expense_items/CardExpense_items.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + expense_items: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardExpense_items = ({ + expense_items, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && expense_items.map((item, index) => ( +
    • + + {item.description} + + +
      + +
      +
    +
    + +
    +
    ExpenseDatetime
    +
    +
    + { dataFormatter.dateTimeFormatter(item.expense_datetime) } +
    +
    +
    + +
    +
    Description
    +
    +
    + { item.description } +
    +
    +
    + +
    +
    ExpenseAmount
    +
    +
    + { item.expense_amount } +
    +
    +
    + +
    +
    ExpenseType
    +
    +
    + { dataFormatter.expense_typesOneListFormatter(item.expense_type) } +
    +
    +
    + +
    +
    Branch
    +
    +
    + { dataFormatter.branchesOneListFormatter(item.branch) } +
    +
    +
    + +
    + + ))} + {!loading && expense_items.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardExpense_items; diff --git a/frontend/src/components/Expense_items/ListExpense_items.tsx b/frontend/src/components/Expense_items/ListExpense_items.tsx new file mode 100644 index 0000000..c634a1a --- /dev/null +++ b/frontend/src/components/Expense_items/ListExpense_items.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + expense_items: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListExpense_items = ({ expense_items, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && expense_items.map((item) => ( +
    + +
    + + +
    +

    ExpenseDatetime

    +

    { dataFormatter.dateTimeFormatter(item.expense_datetime) }

    +
    + +
    +

    Description

    +

    { item.description }

    +
    + +
    +

    ExpenseAmount

    +

    { item.expense_amount }

    +
    + +
    +

    ExpenseType

    +

    { dataFormatter.expense_typesOneListFormatter(item.expense_type) }

    +
    + +
    +

    Branch

    +

    { dataFormatter.branchesOneListFormatter(item.branch) }

    +
    + + + +
    +
    +
    + ))} + {!loading && expense_items.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListExpense_items diff --git a/frontend/src/components/Expense_items/TableExpense_items.tsx b/frontend/src/components/Expense_items/TableExpense_items.tsx new file mode 100644 index 0000000..d3c03cf --- /dev/null +++ b/frontend/src/components/Expense_items/TableExpense_items.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/expense_items/expense_itemsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureExpense_itemsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleExpense_items = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { expense_items, loading, count, notify: expense_itemsNotify, refetch } = useAppSelector((state) => state.expense_items) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (expense_itemsNotify.showNotification) { + notify(expense_itemsNotify.typeNotification, expense_itemsNotify.textNotification); + } + }, [expense_itemsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `expense_items`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={expense_items ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleExpense_items diff --git a/frontend/src/components/Expense_items/configureExpense_itemsCols.tsx b/frontend/src/components/Expense_items/configureExpense_itemsCols.tsx new file mode 100644 index 0000000..801d341 --- /dev/null +++ b/frontend/src/components/Expense_items/configureExpense_itemsCols.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'expense_datetime', + headerName: 'ExpenseDatetime', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.expense_datetime), + + }, + + { + field: 'description', + headerName: 'Description', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'expense_amount', + headerName: 'ExpenseAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'expense_type', + headerName: 'ExpenseType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('expense_types'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'branch', + headerName: 'Branch', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('branches'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Expense_types/CardExpense_types.tsx b/frontend/src/components/Expense_types/CardExpense_types.tsx new file mode 100644 index 0000000..046f1cb --- /dev/null +++ b/frontend/src/components/Expense_types/CardExpense_types.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + expense_types: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardExpense_types = ({ + expense_types, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && expense_types.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    + + ))} + {!loading && expense_types.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardExpense_types; diff --git a/frontend/src/components/Expense_types/ListExpense_types.tsx b/frontend/src/components/Expense_types/ListExpense_types.tsx new file mode 100644 index 0000000..f5ded2c --- /dev/null +++ b/frontend/src/components/Expense_types/ListExpense_types.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + expense_types: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListExpense_types = ({ expense_types, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && expense_types.map((item) => ( +
    + +
    + + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + + + +
    +
    +
    + ))} + {!loading && expense_types.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListExpense_types diff --git a/frontend/src/components/Expense_types/TableExpense_types.tsx b/frontend/src/components/Expense_types/TableExpense_types.tsx new file mode 100644 index 0000000..f271ab8 --- /dev/null +++ b/frontend/src/components/Expense_types/TableExpense_types.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/expense_types/expense_typesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureExpense_typesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleExpense_types = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { expense_types, loading, count, notify: expense_typesNotify, refetch } = useAppSelector((state) => state.expense_types) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (expense_typesNotify.showNotification) { + notify(expense_typesNotify.typeNotification, expense_typesNotify.textNotification); + } + }, [expense_typesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `expense_types`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={expense_types ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleExpense_types diff --git a/frontend/src/components/Expense_types/configureExpense_typesCols.tsx b/frontend/src/components/Expense_types/configureExpense_typesCols.tsx new file mode 100644 index 0000000..acebac6 --- /dev/null +++ b/frontend/src/components/Expense_types/configureExpense_typesCols.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/FooterBar.tsx b/frontend/src/components/FooterBar.tsx new file mode 100644 index 0000000..0acc9c5 --- /dev/null +++ b/frontend/src/components/FooterBar.tsx @@ -0,0 +1,35 @@ +import React, { ReactNode } from 'react' +import { containerMaxW } from '../config' +import Logo from './Logo' + +type Props = { + children?: ReactNode +} + +export default function FooterBar({ children }: Props) { + const year = new Date().getFullYear() + + return ( + + ) +} diff --git a/frontend/src/components/FormCheckRadio.tsx b/frontend/src/components/FormCheckRadio.tsx new file mode 100644 index 0000000..7a9bc31 --- /dev/null +++ b/frontend/src/components/FormCheckRadio.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from 'react' + +type Props = { + children: ReactNode + type: 'checkbox' | 'radio' | 'switch' + label?: string + className?: string +} + +const FormCheckRadio = (props: Props) => { + return ( + + ) +} + +export default FormCheckRadio diff --git a/frontend/src/components/FormCheckRadioGroup.tsx b/frontend/src/components/FormCheckRadioGroup.tsx new file mode 100644 index 0000000..90d98e0 --- /dev/null +++ b/frontend/src/components/FormCheckRadioGroup.tsx @@ -0,0 +1,20 @@ +import { Children, cloneElement, ReactElement, ReactNode } from 'react' + +type Props = { + isColumn?: boolean + children: ReactNode +} + +const FormCheckRadioGroup = (props: Props) => { + return ( +
    + {Children.map(props.children, (child: ReactElement) => + cloneElement(child as ReactElement<{ className?: string }>, { + className: `mr-6 mb-3 last:mr-0 ${(child.props as { className?: string }).className || ''}`, + }), + )} +
    + ) +} + +export default FormCheckRadioGroup diff --git a/frontend/src/components/FormField.tsx b/frontend/src/components/FormField.tsx new file mode 100644 index 0000000..65c8e82 --- /dev/null +++ b/frontend/src/components/FormField.tsx @@ -0,0 +1,80 @@ +import { Children, cloneElement, ReactElement, ReactNode } from 'react' +import BaseIcon from './BaseIcon' +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string + labelFor?: string + help?: string + icons?: string[] | null[] + isBorderless?: boolean + isTransparent?: boolean + hasTextareaHeight?: boolean + children: ReactNode + disabled?: boolean + borderButtom?: boolean + diversity?: boolean + websiteBg?: boolean +} + +const FormField = ({ icons = [], ...props }: Props) => { + const childrenCount = Children.count(props.children) + const bgColor = useAppSelector((state) => state.style.cardsColor); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const corners = useAppSelector((state) => state.style.corners); + const bgWebsiteColor = useAppSelector((state) => state.style.bgLayoutColor); + let elementWrapperClass = '' + + switch (childrenCount) { + case 2: + elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-2' + break + case 3: + elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-3' + } + + const controlClassName = [ + `px-3 py-2 max-w-full border-gray-300 dark:border-dark-700 ${corners} w-full dark:placeholder-gray-400`, + `${focusRing}`, + props.hasTextareaHeight ? 'h-24' : 'h-12', + props.isBorderless ? 'border-0' : 'border', + props.isTransparent ? 'bg-transparent' : `${bgColor} dark:bg-dark-800`, + props.disabled ? 'bg-gray-200 text-gray-100 dark:bg-dark-900 disabled' : '', + props.borderButtom ? `border-0 border-b ${props.diversity ? "border-gray-400" : "placeholder-white border-gray-300/10 border-white"} rounded-none focus:ring-0` : '', + ].join(' '); + + return ( +
    + {props.label && ( + + )} +
    + {Children.map(props.children, (child: ReactElement, index) => ( +
    + {cloneElement(child as ReactElement<{ className?: string }>, { + className: `${controlClassName} ${icons[index] ? 'pl-10' : ''}`, + })} + {icons[index] && ( + + )} +
    + ))} +
    + {props.help && ( +
    {props.help}
    + )} +
    + ) +} + +export default FormField diff --git a/frontend/src/components/FormFilePicker.tsx b/frontend/src/components/FormFilePicker.tsx new file mode 100644 index 0000000..4302edd --- /dev/null +++ b/frontend/src/components/FormFilePicker.tsx @@ -0,0 +1,92 @@ +import {useEffect, useState} from 'react'; +import { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import FileUploader from "./Uploaders/UploadService"; +import { mdiReload } from '@mdi/js'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + accept?: string; + color: ColorButtonKey; + isRoundIcon?: boolean; + path: string; + schema: object; + field: any; + form: any; +}; + +const FormFilePicker = ({ label, icon, accept, color, isRoundIcon, path, + schema, + form, + field, }: Props) => { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + let cornersRight; + if (corners === 'rounded'){ + cornersRight = 'rounded-r' + } else if (corners === 'rounded-lg'){ + cornersRight = 'rounded-r-lg' + }else if (corners === 'rounded-full'){ + cornersRight = 'rounded-r-3xl' + }else{ + cornersRight = '' + } + + useEffect(() => { + if (field.value) { + setFile(field.value[0]); + } + }, [field.value]); + const handleFileChange = async (event) => { + const file = event.currentTarget.files[0]; + setFile(file); + + FileUploader.validate(file, schema); + setLoading(true); + const remoteFile = await FileUploader.upload(path, file, schema); + + form.setFieldValue(field.name, [{ ...remoteFile }]); + setLoading(false); + }; + + const showFilename = !isRoundIcon && file; + + return ( +
    + + {showFilename && !loading && ( +
    + + {file.name} + +
    + )} +
    + ); +}; + +export default FormFilePicker; diff --git a/frontend/src/components/FormImagePicker.tsx b/frontend/src/components/FormImagePicker.tsx new file mode 100644 index 0000000..338d2ab --- /dev/null +++ b/frontend/src/components/FormImagePicker.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from 'react'; +import { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import ImagesUploader from "./Uploaders/ImagesUploader"; +import FileUploader from './Uploaders/UploadService'; +import { mdiReload } from '@mdi/js'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + accept?: string; + color: ColorButtonKey; + isRoundIcon?: boolean; + path: string; + schema: object; + field: any, + form: any, +}; + +const FormImagePicker = ({ label, icon, accept, color, isRoundIcon, path, schema, form, field }: Props) => { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + let cornersRight; + if (corners === 'rounded'){ + cornersRight = 'rounded-r' + } else if (corners === 'rounded-lg'){ + cornersRight = 'rounded-r-lg' + }else if (corners === 'rounded-full'){ + cornersRight = 'rounded-r-3xl' + }else{ + cornersRight = '' + } + + useEffect(() => { + if(field.value) { + setFile(field.value[0]) + } + }, [field.value]) + const handleFileChange = async (event) => { + const file = event.currentTarget.files[0] + setFile(file); + + FileUploader.validate(file, schema); + setLoading(true); + const remoteFile = await FileUploader.upload(path, file, schema); + + form.setFieldValue(field.name, [{...remoteFile}]); + setLoading(false); + }; + + const showFilename = !isRoundIcon && file; + + return ( +
    + + {showFilename && !loading && ( +
    + + {file.name} + +
    + )} +
    + ); +}; + +export default FormImagePicker; diff --git a/frontend/src/components/Group_menus/CardGroup_menus.tsx b/frontend/src/components/Group_menus/CardGroup_menus.tsx new file mode 100644 index 0000000..b47ebae --- /dev/null +++ b/frontend/src/components/Group_menus/CardGroup_menus.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + group_menus: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardGroup_menus = ({ + group_menus, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && group_menus.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    +
    IsAdmin
    +
    +
    + { dataFormatter.booleanFormatter(item.is_admin) } +
    +
    +
    + +
    + + ))} + {!loading && group_menus.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardGroup_menus; diff --git a/frontend/src/components/Group_menus/ListGroup_menus.tsx b/frontend/src/components/Group_menus/ListGroup_menus.tsx new file mode 100644 index 0000000..8da6162 --- /dev/null +++ b/frontend/src/components/Group_menus/ListGroup_menus.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + group_menus: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListGroup_menus = ({ group_menus, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && group_menus.map((item) => ( +
    + +
    + + +
    +

    Name

    +

    { item.name }

    +
    + +
    +

    IsAdmin

    +

    { dataFormatter.booleanFormatter(item.is_admin) }

    +
    + + + +
    +
    +
    + ))} + {!loading && group_menus.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListGroup_menus diff --git a/frontend/src/components/Group_menus/TableGroup_menus.tsx b/frontend/src/components/Group_menus/TableGroup_menus.tsx new file mode 100644 index 0000000..e7b7c84 --- /dev/null +++ b/frontend/src/components/Group_menus/TableGroup_menus.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/group_menus/group_menusSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureGroup_menusCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleGroup_menus = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { group_menus, loading, count, notify: group_menusNotify, refetch } = useAppSelector((state) => state.group_menus) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (group_menusNotify.showNotification) { + notify(group_menusNotify.typeNotification, group_menusNotify.textNotification); + } + }, [group_menusNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `group_menus`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={group_menus ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleGroup_menus diff --git a/frontend/src/components/Group_menus/configureGroup_menusCols.tsx b/frontend/src/components/Group_menus/configureGroup_menusCols.tsx new file mode 100644 index 0000000..26e679e --- /dev/null +++ b/frontend/src/components/Group_menus/configureGroup_menusCols.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'is_admin', + headerName: 'IsAdmin', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Guarantor/CardGuarantor.tsx b/frontend/src/components/Guarantor/CardGuarantor.tsx new file mode 100644 index 0000000..2f2d8ee --- /dev/null +++ b/frontend/src/components/Guarantor/CardGuarantor.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + guarantor: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardGuarantor = ({ + guarantor, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && guarantor.map((item, index) => ( +
    • + + {item.full_name} + + +
      + +
      +
    +
    + +
    +
    FullName
    +
    +
    + { item.full_name } +
    +
    +
    + +
    +
    Sex
    +
    +
    + { item.sex } +
    +
    +
    + +
    +
    DateofBirth
    +
    +
    + { dataFormatter.dateTimeFormatter(item.date_of_birth) } +
    +
    +
    + +
    +
    DocumentType
    +
    +
    + { item.document_type } +
    +
    +
    + +
    +
    DocumentNumber
    +
    +
    + { item.document_number } +
    +
    +
    + +
    +
    PhoneNumber
    +
    +
    + { item.phone_number } +
    +
    +
    + +
    +
    Province
    +
    +
    + { dataFormatter.provincesOneListFormatter(item.province) } +
    +
    +
    + +
    +
    District
    +
    +
    + { dataFormatter.districtsOneListFormatter(item.district) } +
    +
    +
    + +
    +
    Commune
    +
    +
    + { dataFormatter.communesOneListFormatter(item.commune) } +
    +
    +
    + +
    +
    Village
    +
    +
    + { dataFormatter.villagesOneListFormatter(item.village) } +
    +
    +
    + +
    +
    FullAddressInput
    +
    +
    + { item.full_address_input } +
    +
    +
    + +
    + + ))} + {!loading && guarantor.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardGuarantor; diff --git a/frontend/src/components/Guarantor/ListGuarantor.tsx b/frontend/src/components/Guarantor/ListGuarantor.tsx new file mode 100644 index 0000000..c6d4d76 --- /dev/null +++ b/frontend/src/components/Guarantor/ListGuarantor.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + guarantor: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListGuarantor = ({ guarantor, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && guarantor.map((item) => ( +
    + +
    + + +
    +

    FullName

    +

    { item.full_name }

    +
    + +
    +

    Sex

    +

    { item.sex }

    +
    + +
    +

    DateofBirth

    +

    { dataFormatter.dateTimeFormatter(item.date_of_birth) }

    +
    + +
    +

    DocumentType

    +

    { item.document_type }

    +
    + +
    +

    DocumentNumber

    +

    { item.document_number }

    +
    + +
    +

    PhoneNumber

    +

    { item.phone_number }

    +
    + +
    +

    Province

    +

    { dataFormatter.provincesOneListFormatter(item.province) }

    +
    + +
    +

    District

    +

    { dataFormatter.districtsOneListFormatter(item.district) }

    +
    + +
    +

    Commune

    +

    { dataFormatter.communesOneListFormatter(item.commune) }

    +
    + +
    +

    Village

    +

    { dataFormatter.villagesOneListFormatter(item.village) }

    +
    + +
    +

    FullAddressInput

    +

    { item.full_address_input }

    +
    + + + +
    +
    +
    + ))} + {!loading && guarantor.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListGuarantor diff --git a/frontend/src/components/Guarantor/TableGuarantor.tsx b/frontend/src/components/Guarantor/TableGuarantor.tsx new file mode 100644 index 0000000..45e88a9 --- /dev/null +++ b/frontend/src/components/Guarantor/TableGuarantor.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/guarantor/guarantorSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureGuarantorCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleGuarantor = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { guarantor, loading, count, notify: guarantorNotify, refetch } = useAppSelector((state) => state.guarantor) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (guarantorNotify.showNotification) { + notify(guarantorNotify.typeNotification, guarantorNotify.textNotification); + } + }, [guarantorNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `guarantor`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={guarantor ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleGuarantor diff --git a/frontend/src/components/Guarantor/configureGuarantorCols.tsx b/frontend/src/components/Guarantor/configureGuarantorCols.tsx new file mode 100644 index 0000000..93d2703 --- /dev/null +++ b/frontend/src/components/Guarantor/configureGuarantorCols.tsx @@ -0,0 +1,226 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'full_name', + headerName: 'FullName', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'sex', + headerName: 'Sex', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'date_of_birth', + headerName: 'DateofBirth', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.date_of_birth), + + }, + + { + field: 'document_type', + headerName: 'DocumentType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'document_number', + headerName: 'DocumentNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'phone_number', + headerName: 'PhoneNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'province', + headerName: 'Province', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('provinces'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'district', + headerName: 'District', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('districts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'commune', + headerName: 'Commune', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('communes'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'village', + headerName: 'Village', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('villages'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'full_address_input', + headerName: 'FullAddressInput', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/IconRounded.tsx b/frontend/src/components/IconRounded.tsx new file mode 100644 index 0000000..fc45023 --- /dev/null +++ b/frontend/src/components/IconRounded.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { ColorKey } from '../interfaces' +import { colorsBgLight, colorsText } from '../colors' +import BaseIcon from './BaseIcon' + +type Props = { + icon: string + color: ColorKey + w?: string + h?: string + bg?: boolean + className?: string +} + +export default function IconRounded({ + icon, + color, + w = 'w-12', + h = 'h-12', + bg = false, + className = '', +}: Props) { + const classAddon = bg ? colorsBgLight[color] : `${colorsText[color]} bg-gray-50 dark:bg-slate-800` + + return ( + + ) +} diff --git a/frontend/src/components/Interest_rates/CardInterest_rates.tsx b/frontend/src/components/Interest_rates/CardInterest_rates.tsx new file mode 100644 index 0000000..21d7bc3 --- /dev/null +++ b/frontend/src/components/Interest_rates/CardInterest_rates.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + interest_rates: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardInterest_rates = ({ + interest_rates, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && interest_rates.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Code
    +
    +
    + { item.code } +
    +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    +
    Rate
    +
    +
    + { item.rate } +
    +
    +
    + +
    +
    CommissionRate
    +
    +
    + { item.commission_rate } +
    +
    +
    + +
    +
    Interval
    +
    +
    + { item.interval } +
    +
    +
    + +
    +
    Sort
    +
    +
    + { item.sort } +
    +
    +
    + +
    +
    CSS
    +
    +
    + { item.css } +
    +
    +
    + +
    +
    Setting
    +
    +
    + { item.setting } +
    +
    +
    + +
    + + ))} + {!loading && interest_rates.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardInterest_rates; diff --git a/frontend/src/components/Interest_rates/ListInterest_rates.tsx b/frontend/src/components/Interest_rates/ListInterest_rates.tsx new file mode 100644 index 0000000..9b8ac6a --- /dev/null +++ b/frontend/src/components/Interest_rates/ListInterest_rates.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + interest_rates: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListInterest_rates = ({ interest_rates, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && interest_rates.map((item) => ( +
    + +
    + + +
    +

    Code

    +

    { item.code }

    +
    + +
    +

    Name

    +

    { item.name }

    +
    + +
    +

    Rate

    +

    { item.rate }

    +
    + +
    +

    CommissionRate

    +

    { item.commission_rate }

    +
    + +
    +

    Interval

    +

    { item.interval }

    +
    + +
    +

    Sort

    +

    { item.sort }

    +
    + +
    +

    CSS

    +

    { item.css }

    +
    + +
    +

    Setting

    +

    { item.setting }

    +
    + + + +
    +
    +
    + ))} + {!loading && interest_rates.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListInterest_rates diff --git a/frontend/src/components/Interest_rates/TableInterest_rates.tsx b/frontend/src/components/Interest_rates/TableInterest_rates.tsx new file mode 100644 index 0000000..22701b5 --- /dev/null +++ b/frontend/src/components/Interest_rates/TableInterest_rates.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/interest_rates/interest_ratesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureInterest_ratesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleInterest_rates = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { interest_rates, loading, count, notify: interest_ratesNotify, refetch } = useAppSelector((state) => state.interest_rates) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (interest_ratesNotify.showNotification) { + notify(interest_ratesNotify.typeNotification, interest_ratesNotify.textNotification); + } + }, [interest_ratesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `interest_rates`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={interest_rates ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleInterest_rates diff --git a/frontend/src/components/Interest_rates/configureInterest_ratesCols.tsx b/frontend/src/components/Interest_rates/configureInterest_ratesCols.tsx new file mode 100644 index 0000000..256e60e --- /dev/null +++ b/frontend/src/components/Interest_rates/configureInterest_ratesCols.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'code', + headerName: 'Code', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'rate', + headerName: 'Rate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'commission_rate', + headerName: 'CommissionRate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'interval', + headerName: 'Interval', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'sort', + headerName: 'Sort', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'css', + headerName: 'CSS', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'setting', + headerName: 'Setting', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/ListActionsPopover.tsx b/frontend/src/components/ListActionsPopover.tsx new file mode 100644 index 0000000..e07e678 --- /dev/null +++ b/frontend/src/components/ListActionsPopover.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import Link from 'next/link'; +import Button from '@mui/material/Button'; +import BaseIcon from './BaseIcon'; +import { + mdiDotsVertical, + mdiEye, + mdiPencilOutline, + mdiTrashCan, +} from '@mdi/js'; +import Popover from '@mui/material/Popover'; +import { IconButton } from '@mui/material'; + +type Props = { + itemId: string; + onDelete: (id: string) => void; + hasUpdatePermission: boolean; + className?: string; + iconClassName?: string; + pathEdit: string; + pathView: string; +}; + +const ListActionsPopover = ({ + itemId, + onDelete, + hasUpdatePermission, + className, + iconClassName, + pathEdit, + pathView, + }: Props) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const linkView = pathView; + const linkEdit = pathEdit; + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'simple-popover' : undefined; + + return ( + <> + + + + +
    + + {hasUpdatePermission && ( + + )} + {hasUpdatePermission && ( + + )} +
    +
    + + ); +}; + +export default ListActionsPopover; diff --git a/frontend/src/components/LoadingSpinner.tsx b/frontend/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..b594b2a --- /dev/null +++ b/frontend/src/components/LoadingSpinner.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const LoadingSpinner = () => { + return ( +
    +
    +
    +
    +
    +
    + ); +}; + +export default LoadingSpinner; \ No newline at end of file diff --git a/frontend/src/components/Loan_status/CardLoan_status.tsx b/frontend/src/components/Loan_status/CardLoan_status.tsx new file mode 100644 index 0000000..6e51686 --- /dev/null +++ b/frontend/src/components/Loan_status/CardLoan_status.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + loan_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLoan_status = ({ + loan_status, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && loan_status.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    +
    CSS
    +
    +
    + { item.css } +
    +
    +
    + +
    + + ))} + {!loading && loan_status.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardLoan_status; diff --git a/frontend/src/components/Loan_status/ListLoan_status.tsx b/frontend/src/components/Loan_status/ListLoan_status.tsx new file mode 100644 index 0000000..1054937 --- /dev/null +++ b/frontend/src/components/Loan_status/ListLoan_status.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + loan_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListLoan_status = ({ loan_status, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && loan_status.map((item) => ( +
    + +
    + + +
    +

    Name

    +

    { item.name }

    +
    + +
    +

    CSS

    +

    { item.css }

    +
    + + + +
    +
    +
    + ))} + {!loading && loan_status.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListLoan_status diff --git a/frontend/src/components/Loan_status/TableLoan_status.tsx b/frontend/src/components/Loan_status/TableLoan_status.tsx new file mode 100644 index 0000000..fac5762 --- /dev/null +++ b/frontend/src/components/Loan_status/TableLoan_status.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/loan_status/loan_statusSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureLoan_statusCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleLoan_status = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { loan_status, loading, count, notify: loan_statusNotify, refetch } = useAppSelector((state) => state.loan_status) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (loan_statusNotify.showNotification) { + notify(loan_statusNotify.typeNotification, loan_statusNotify.textNotification); + } + }, [loan_statusNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `loan_status`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={loan_status ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleLoan_status diff --git a/frontend/src/components/Loan_status/configureLoan_statusCols.tsx b/frontend/src/components/Loan_status/configureLoan_statusCols.tsx new file mode 100644 index 0000000..11a2afa --- /dev/null +++ b/frontend/src/components/Loan_status/configureLoan_statusCols.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'css', + headerName: 'CSS', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Loan_types/CardLoan_types.tsx b/frontend/src/components/Loan_types/CardLoan_types.tsx new file mode 100644 index 0000000..1b6a2a0 --- /dev/null +++ b/frontend/src/components/Loan_types/CardLoan_types.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + loan_types: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLoan_types = ({ + loan_types, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && loan_types.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    + + ))} + {!loading && loan_types.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardLoan_types; diff --git a/frontend/src/components/Loan_types/ListLoan_types.tsx b/frontend/src/components/Loan_types/ListLoan_types.tsx new file mode 100644 index 0000000..e85d7c8 --- /dev/null +++ b/frontend/src/components/Loan_types/ListLoan_types.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + loan_types: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListLoan_types = ({ loan_types, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && loan_types.map((item) => ( +
    + +
    + + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + + + +
    +
    +
    + ))} + {!loading && loan_types.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListLoan_types diff --git a/frontend/src/components/Loan_types/TableLoan_types.tsx b/frontend/src/components/Loan_types/TableLoan_types.tsx new file mode 100644 index 0000000..d98b14d --- /dev/null +++ b/frontend/src/components/Loan_types/TableLoan_types.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/loan_types/loan_typesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureLoan_typesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleLoan_types = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { loan_types, loading, count, notify: loan_typesNotify, refetch } = useAppSelector((state) => state.loan_types) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (loan_typesNotify.showNotification) { + notify(loan_typesNotify.typeNotification, loan_typesNotify.textNotification); + } + }, [loan_typesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `loan_types`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={loan_types ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleLoan_types diff --git a/frontend/src/components/Loan_types/configureLoan_typesCols.tsx b/frontend/src/components/Loan_types/configureLoan_typesCols.tsx new file mode 100644 index 0000000..92b8299 --- /dev/null +++ b/frontend/src/components/Loan_types/configureLoan_typesCols.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Loans/CardLoans.tsx b/frontend/src/components/Loans/CardLoans.tsx new file mode 100644 index 0000000..a4db46f --- /dev/null +++ b/frontend/src/components/Loans/CardLoans.tsx @@ -0,0 +1,282 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + loans: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLoans = ({ + loans, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && loans.map((item, index) => ( +
    • + + {item.code} + + +
      + +
      +
    +
    + +
    +
    Code
    +
    +
    + { item.code } +
    +
    +
    + +
    +
    PrincipalAmount
    +
    +
    + { item.principal_amount } +
    +
    +
    + +
    +
    Term
    +
    +
    + { item.term } +
    +
    +
    + +
    +
    PendingAmount
    +
    +
    + { item.pending_amount } +
    +
    +
    + +
    +
    LastPendingAmount
    +
    +
    + { item.last_pending_amount } +
    +
    +
    + +
    +
    Rate
    +
    +
    + { item.rate } +
    +
    +
    + +
    +
    CommissionRate
    +
    +
    + { item.commission_rate } +
    +
    +
    + +
    +
    RegistrationDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.registration_date) } +
    +
    +
    + +
    +
    StartedPaymentDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.started_payment_date) } +
    +
    +
    + +
    +
    LastPaymentDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.last_payment_date) } +
    +
    +
    + +
    +
    FinishPaymentDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.finish_payment_date) } +
    +
    +
    + +
    +
    FinishDiscount
    +
    +
    + { item.finish_discount } +
    +
    +
    + +
    +
    FinishDiscountAmount
    +
    +
    + { item.finish_discount_amount } +
    +
    +
    + +
    +
    AdminRate
    +
    +
    + { item.admin_rate } +
    +
    +
    + +
    +
    AdminAmount
    +
    +
    + { item.admin_amount } +
    +
    +
    + +
    +
    Purpose
    +
    +
    + { item.purpose } +
    +
    +
    + +
    +
    Status
    +
    +
    + { dataFormatter.loan_statusOneListFormatter(item.status) } +
    +
    +
    + +
    +
    Client
    +
    +
    + { dataFormatter.clientsOneListFormatter(item.client) } +
    +
    +
    + +
    +
    Staff
    +
    +
    + { dataFormatter.staffsOneListFormatter(item.staff) } +
    +
    +
    + +
    +
    InterestRate
    +
    +
    + { dataFormatter.interest_ratesOneListFormatter(item.interest_rate) } +
    +
    +
    + +
    +
    Branch
    +
    +
    + { dataFormatter.branchesOneListFormatter(item.branch) } +
    +
    +
    + +
    +
    LoanType
    +
    +
    + { dataFormatter.loan_typesOneListFormatter(item.loan_type) } +
    +
    +
    + +
    + + ))} + {!loading && loans.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardLoans; diff --git a/frontend/src/components/Loans/ListLoans.tsx b/frontend/src/components/Loans/ListLoans.tsx new file mode 100644 index 0000000..9e6bc66 --- /dev/null +++ b/frontend/src/components/Loans/ListLoans.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + loans: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListLoans = ({ loans, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && loans.map((item) => ( +
    + +
    + + +
    +

    Code

    +

    { item.code }

    +
    + +
    +

    PrincipalAmount

    +

    { item.principal_amount }

    +
    + +
    +

    Term

    +

    { item.term }

    +
    + +
    +

    PendingAmount

    +

    { item.pending_amount }

    +
    + +
    +

    LastPendingAmount

    +

    { item.last_pending_amount }

    +
    + +
    +

    Rate

    +

    { item.rate }

    +
    + +
    +

    CommissionRate

    +

    { item.commission_rate }

    +
    + +
    +

    RegistrationDate

    +

    { dataFormatter.dateTimeFormatter(item.registration_date) }

    +
    + +
    +

    StartedPaymentDate

    +

    { dataFormatter.dateTimeFormatter(item.started_payment_date) }

    +
    + +
    +

    LastPaymentDate

    +

    { dataFormatter.dateTimeFormatter(item.last_payment_date) }

    +
    + +
    +

    FinishPaymentDate

    +

    { dataFormatter.dateTimeFormatter(item.finish_payment_date) }

    +
    + +
    +

    FinishDiscount

    +

    { item.finish_discount }

    +
    + +
    +

    FinishDiscountAmount

    +

    { item.finish_discount_amount }

    +
    + +
    +

    AdminRate

    +

    { item.admin_rate }

    +
    + +
    +

    AdminAmount

    +

    { item.admin_amount }

    +
    + +
    +

    Purpose

    +

    { item.purpose }

    +
    + +
    +

    Status

    +

    { dataFormatter.loan_statusOneListFormatter(item.status) }

    +
    + +
    +

    Client

    +

    { dataFormatter.clientsOneListFormatter(item.client) }

    +
    + +
    +

    Staff

    +

    { dataFormatter.staffsOneListFormatter(item.staff) }

    +
    + +
    +

    InterestRate

    +

    { dataFormatter.interest_ratesOneListFormatter(item.interest_rate) }

    +
    + +
    +

    Branch

    +

    { dataFormatter.branchesOneListFormatter(item.branch) }

    +
    + +
    +

    LoanType

    +

    { dataFormatter.loan_typesOneListFormatter(item.loan_type) }

    +
    + + + +
    +
    +
    + ))} + {!loading && loans.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListLoans diff --git a/frontend/src/components/Loans/TableLoans.tsx b/frontend/src/components/Loans/TableLoans.tsx new file mode 100644 index 0000000..e0149b5 --- /dev/null +++ b/frontend/src/components/Loans/TableLoans.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/loans/loansSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureLoansCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleLoans = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { loans, loading, count, notify: loansNotify, refetch } = useAppSelector((state) => state.loans) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (loansNotify.showNotification) { + notify(loansNotify.typeNotification, loansNotify.textNotification); + } + }, [loansNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `loans`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={loans ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleLoans diff --git a/frontend/src/components/Loans/configureLoansCols.tsx b/frontend/src/components/Loans/configureLoansCols.tsx new file mode 100644 index 0000000..6d8956d --- /dev/null +++ b/frontend/src/components/Loans/configureLoansCols.tsx @@ -0,0 +1,402 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'code', + headerName: 'Code', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'principal_amount', + headerName: 'PrincipalAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'term', + headerName: 'Term', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'pending_amount', + headerName: 'PendingAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'last_pending_amount', + headerName: 'LastPendingAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'rate', + headerName: 'Rate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'commission_rate', + headerName: 'CommissionRate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'registration_date', + headerName: 'RegistrationDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.registration_date), + + }, + + { + field: 'started_payment_date', + headerName: 'StartedPaymentDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.started_payment_date), + + }, + + { + field: 'last_payment_date', + headerName: 'LastPaymentDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.last_payment_date), + + }, + + { + field: 'finish_payment_date', + headerName: 'FinishPaymentDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.finish_payment_date), + + }, + + { + field: 'finish_discount', + headerName: 'FinishDiscount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'finish_discount_amount', + headerName: 'FinishDiscountAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'admin_rate', + headerName: 'AdminRate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'admin_amount', + headerName: 'AdminAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'purpose', + headerName: 'Purpose', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('loan_status'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'client', + headerName: 'Client', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('clients'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'staff', + headerName: 'Staff', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('staffs'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'interest_rate', + headerName: 'InterestRate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('interest_rates'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'branch', + headerName: 'Branch', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('branches'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'loan_type', + headerName: 'LoanType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('loan_types'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Logo/index.tsx b/frontend/src/components/Logo/index.tsx new file mode 100644 index 0000000..a582e29 --- /dev/null +++ b/frontend/src/components/Logo/index.tsx @@ -0,0 +1,15 @@ +import React from 'react' + +type Props = { + className?: string +} + +export default function Logo({ className = '' }: Props) { + return ( + {'Flatlogic + + ) +} diff --git a/frontend/src/components/Members/CardMembers.tsx b/frontend/src/components/Members/CardMembers.tsx new file mode 100644 index 0000000..6913753 --- /dev/null +++ b/frontend/src/components/Members/CardMembers.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + members: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardMembers = ({ + members, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && members.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    PhoneNumber
    +
    +
    + { item.phone_number } +
    +
    +
    + +
    +
    Sex
    +
    +
    + { dataFormatter.sexesOneListFormatter(item.sex) } +
    +
    +
    + +
    + + ))} + {!loading && members.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardMembers; diff --git a/frontend/src/components/Members/ListMembers.tsx b/frontend/src/components/Members/ListMembers.tsx new file mode 100644 index 0000000..cf69822 --- /dev/null +++ b/frontend/src/components/Members/ListMembers.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + members: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListMembers = ({ members, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && members.map((item) => ( +
    + +
    + + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    PhoneNumber

    +

    { item.phone_number }

    +
    + +
    +

    Sex

    +

    { dataFormatter.sexesOneListFormatter(item.sex) }

    +
    + + + +
    +
    +
    + ))} + {!loading && members.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListMembers diff --git a/frontend/src/components/Members/TableMembers.tsx b/frontend/src/components/Members/TableMembers.tsx new file mode 100644 index 0000000..046f4b3 --- /dev/null +++ b/frontend/src/components/Members/TableMembers.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/members/membersSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureMembersCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleMembers = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { members, loading, count, notify: membersNotify, refetch } = useAppSelector((state) => state.members) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (membersNotify.showNotification) { + notify(membersNotify.typeNotification, membersNotify.textNotification); + } + }, [membersNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `members`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={members ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleMembers diff --git a/frontend/src/components/Members/configureMembersCols.tsx b/frontend/src/components/Members/configureMembersCols.tsx new file mode 100644 index 0000000..c0a4b85 --- /dev/null +++ b/frontend/src/components/Members/configureMembersCols.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'phone_number', + headerName: 'PhoneNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'sex', + headerName: 'Sex', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('sexes'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Menus/CardMenus.tsx b/frontend/src/components/Menus/CardMenus.tsx new file mode 100644 index 0000000..b107572 --- /dev/null +++ b/frontend/src/components/Menus/CardMenus.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + menus: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardMenus = ({ + menus, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && menus.map((item, index) => ( +
    • + + {item.label} + + +
      + +
      +
    +
    + +
    +
    Parent
    +
    +
    + { dataFormatter.menusOneListFormatter(item.parent) } +
    +
    +
    + +
    +
    Label
    +
    +
    + { item.label } +
    +
    +
    + +
    +
    URL
    +
    +
    + { item.url } +
    +
    +
    + +
    +
    ActiveURL
    +
    +
    + { item.active_url } +
    +
    +
    + +
    +
    Permission
    +
    +
    + { item.permission } +
    +
    +
    + +
    +
    Icon
    +
    +
    + { item.icon } +
    +
    +
    + +
    +
    URL
    +
    +
    + { dataFormatter.urlsOneListFormatter(item.url) } +
    +
    +
    + +
    +
    Group
    +
    +
    + { dataFormatter.group_menusOneListFormatter(item.group) } +
    +
    +
    + +
    + + ))} + {!loading && menus.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardMenus; diff --git a/frontend/src/components/Menus/ListMenus.tsx b/frontend/src/components/Menus/ListMenus.tsx new file mode 100644 index 0000000..266b7b2 --- /dev/null +++ b/frontend/src/components/Menus/ListMenus.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + menus: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListMenus = ({ menus, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && menus.map((item) => ( +
    + +
    + + +
    +

    Parent

    +

    { dataFormatter.menusOneListFormatter(item.parent) }

    +
    + +
    +

    Label

    +

    { item.label }

    +
    + +
    +

    URL

    +

    { item.url }

    +
    + +
    +

    ActiveURL

    +

    { item.active_url }

    +
    + +
    +

    Permission

    +

    { item.permission }

    +
    + +
    +

    Icon

    +

    { item.icon }

    +
    + +
    +

    URL

    +

    { dataFormatter.urlsOneListFormatter(item.url) }

    +
    + +
    +

    Group

    +

    { dataFormatter.group_menusOneListFormatter(item.group) }

    +
    + + + +
    +
    +
    + ))} + {!loading && menus.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListMenus diff --git a/frontend/src/components/Menus/TableMenus.tsx b/frontend/src/components/Menus/TableMenus.tsx new file mode 100644 index 0000000..e2bdf94 --- /dev/null +++ b/frontend/src/components/Menus/TableMenus.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/menus/menusSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureMenusCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleMenus = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { menus, loading, count, notify: menusNotify, refetch } = useAppSelector((state) => state.menus) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (menusNotify.showNotification) { + notify(menusNotify.typeNotification, menusNotify.textNotification); + } + }, [menusNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `menus`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={menus ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleMenus diff --git a/frontend/src/components/Menus/configureMenusCols.tsx b/frontend/src/components/Menus/configureMenusCols.tsx new file mode 100644 index 0000000..9e33701 --- /dev/null +++ b/frontend/src/components/Menus/configureMenusCols.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'parent', + headerName: 'Parent', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('menus'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'label', + headerName: 'Label', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'url', + headerName: 'URL', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'active_url', + headerName: 'ActiveURL', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'permission', + headerName: 'Permission', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'icon', + headerName: 'Icon', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'url', + headerName: 'URL', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('urls'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'group', + headerName: 'Group', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('group_menus'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx new file mode 100644 index 0000000..1af817f --- /dev/null +++ b/frontend/src/components/NavBar.tsx @@ -0,0 +1,57 @@ +import React, { ReactNode, useState, useEffect } from 'react' +import { mdiClose, mdiDotsVertical } from '@mdi/js' +import { containerMaxW } from '../config' +import BaseIcon from './BaseIcon' +import NavBarItemPlain from './NavBarItemPlain' +import NavBarMenuList from './NavBarMenuList' +import { MenuNavBarItem } from '../interfaces' +import { useAppSelector } from '../stores/hooks'; + +type Props = { + menu: MenuNavBarItem[] + className: string + children: ReactNode +} + +export default function NavBar({ menu, className = '', children }: Props) { + const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false) + const [isScrolled, setIsScrolled] = useState(false); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + + useEffect(() => { + const handleScroll = () => { + const scrolled = window.scrollY > 0; + setIsScrolled(scrolled); + }; + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + const handleMenuNavBarToggleClick = () => { + setIsMenuNavBarActive(!isMenuNavBarActive) + } + + return ( + + ) +} diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx new file mode 100644 index 0000000..72935e6 --- /dev/null +++ b/frontend/src/components/NavBarItem.tsx @@ -0,0 +1,132 @@ +import React, {useEffect, useRef} from 'react' +import Link from 'next/link' +import { useState } from 'react' +import { mdiChevronUp, mdiChevronDown } from '@mdi/js' +import BaseDivider from './BaseDivider' +import BaseIcon from './BaseIcon' +import UserAvatarCurrentUser from './UserAvatarCurrentUser' +import NavBarMenuList from './NavBarMenuList' +import { useAppDispatch, useAppSelector } from '../stores/hooks' +import { MenuNavBarItem } from '../interfaces' +import { setDarkMode } from '../stores/styleSlice' +import { logoutUser } from '../stores/authSlice' +import { useRouter } from 'next/router'; +import ClickOutside from "./ClickOutside"; + +type Props = { + item: MenuNavBarItem +} + +export default function NavBarItem({ item }: Props) { + const router = useRouter(); + const dispatch = useAppDispatch(); + const excludedRef = useRef(null); + + const navBarItemLabelActiveColorStyle = useAppSelector( + (state) => state.style.navBarItemLabelActiveColorStyle + ) + const navBarItemLabelStyle = useAppSelector((state) => state.style.navBarItemLabelStyle) + const navBarItemLabelHoverStyle = useAppSelector((state) => state.style.navBarItemLabelHoverStyle) + + const currentUser = useAppSelector((state) => state.auth.currentUser); + + const userName = `${currentUser?.firstName ? currentUser?.firstName : ""} ${currentUser?.lastName ? currentUser?.lastName : ""}`; + + const [isDropdownActive, setIsDropdownActive] = useState(false) + + useEffect(() => { + return () => setIsDropdownActive(false); + }, [router.pathname]); + + const componentClass = [ + 'block lg:flex items-center relative cursor-pointer', + isDropdownActive + ? `${navBarItemLabelActiveColorStyle} dark:text-slate-400` + : `${navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${navBarItemLabelHoverStyle}`, + item.menu ? 'lg:py-2 lg:px-3' : 'py-2 px-3', + item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '', + ].join(' ') + + const itemLabel = item.isCurrentUser ? userName : item.label + + const handleMenuClick = () => { + if (item.menu) { + setIsDropdownActive(!isDropdownActive) + } + + if (item.isToggleLightDark) { + dispatch(setDarkMode(null)) + } + + if(item.isLogout) { + dispatch(logoutUser()) + router.push('/login') + } + } + + const getItemId = (label) => { + switch (label) { + case 'Light/Dark': + return 'themeToggle'; + case 'Log out': + return 'logout'; + default: + return undefined; + } + }; + + const NavBarItemComponentContents = ( + <> +
    + {item.icon && } + + {itemLabel} + + {item.isCurrentUser && } + {item.menu && ( + + )} +
    + {item.menu && ( +
    + setIsDropdownActive(false)} excludedElements={[excludedRef]}> + + +
    + )} + + ) + + if (item.isDivider) { + return + } + + if (item.href) { + return ( + + {NavBarItemComponentContents} + + ) + } + + return
    {NavBarItemComponentContents}
    +} diff --git a/frontend/src/components/NavBarItemPlain.tsx b/frontend/src/components/NavBarItemPlain.tsx new file mode 100644 index 0000000..5589728 --- /dev/null +++ b/frontend/src/components/NavBarItemPlain.tsx @@ -0,0 +1,30 @@ +import React, { ReactNode } from 'react' +import { useAppSelector } from '../stores/hooks' + +type Props = { + display?: string + useMargin?: boolean + children?: ReactNode + onClick?: (e: React.MouseEvent) => void +} + +export default function NavBarItemPlain({ + display = 'flex', + useMargin = false, + onClick, + children, +}: Props) { + const navBarItemLabelStyle = useAppSelector((state) => state.style.navBarItemLabelStyle) + const navBarItemLabelHoverStyle = useAppSelector((state) => state.style.navBarItemLabelHoverStyle) + + const classBase = 'items-center cursor-pointer dark:text-white dark:hover:text-slate-400' + const classAddon = `${display} ${navBarItemLabelStyle} ${navBarItemLabelHoverStyle} ${ + useMargin ? 'my-2 mx-3' : 'py-2 px-3' + }` + + return ( +
    + {children} +
    + ) +} diff --git a/frontend/src/components/NavBarMenuList.tsx b/frontend/src/components/NavBarMenuList.tsx new file mode 100644 index 0000000..0896428 --- /dev/null +++ b/frontend/src/components/NavBarMenuList.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { MenuNavBarItem } from '../interfaces' +import NavBarItem from './NavBarItem' + +type Props = { + menu: MenuNavBarItem[] +} + +export default function NavBarMenuList({ menu }: Props) { + return ( + <> + {menu.map((item, index) => ( +
    + +
    + ))} + + ) +} diff --git a/frontend/src/components/NotificationBar.tsx b/frontend/src/components/NotificationBar.tsx new file mode 100644 index 0000000..a3ade58 --- /dev/null +++ b/frontend/src/components/NotificationBar.tsx @@ -0,0 +1,57 @@ +import { mdiClose } from '@mdi/js' +import React, { ReactNode, useState } from 'react' +import { ColorKey } from '../interfaces' +import { colorsBgLight, colorsOutline } from '../colors' +import BaseButton from './BaseButton' +import BaseIcon from './BaseIcon' + +type Props = { + color: ColorKey + icon?: string + outline?: boolean + children: ReactNode + button?: ReactNode +} + +const NotificationBar = ({ outline = false, children, ...props }: Props) => { + const componentColorClass = outline ? colorsOutline[props.color] : colorsBgLight[props.color] + + const [isDismissed, setIsDismissed] = useState(false) + + const dismiss = (e: React.MouseEvent) => { + e.preventDefault() + + setIsDismissed(true) + } + + if (isDismissed) { + return null + } + + return ( +
    +
    +
    + {props.icon && ( + + )} + {children} +
    + {props.button} + {!props.button && ( + + )} +
    +
    + ) +} + +export default NotificationBar diff --git a/frontend/src/components/OverlayLayer.tsx b/frontend/src/components/OverlayLayer.tsx new file mode 100644 index 0000000..4cb9e6f --- /dev/null +++ b/frontend/src/components/OverlayLayer.tsx @@ -0,0 +1,41 @@ +import React, { ReactNode } from 'react' +import { useAppSelector } from '../stores/hooks' + +type Props = { + zIndex?: string + type?: string + children?: ReactNode + className?: string + onClick: (e: React.MouseEvent) => void +} + +export default function OverlayLayer({ + zIndex = 'z-50', + type = 'flex', + children, + className, + ...props +}: Props) { + const overlayStyle = useAppSelector((state) => state.style.overlayStyle) + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault() + + if (props.onClick) { + props.onClick(e) + } + } + + return ( +
    +
    + + {children} +
    + ) +} diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx new file mode 100644 index 0000000..ec68ef3 --- /dev/null +++ b/frontend/src/components/Pagination.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { + mdiChevronDoubleLeft, + mdiChevronDoubleRight, + mdiChevronLeft, + mdiChevronRight, +} from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +type Props = { + currentPage: number; + numPages: number; + setCurrentPage: any; +}; + +export const Pagination = ({ + currentPage, + numPages, + setCurrentPage, +}: Props) => { + return ( +
    + {currentPage === 0 && ( +
    + + +
    + )} + {currentPage !== 0 && ( +
    +
    setCurrentPage(0)}> + +
    +
    setCurrentPage(currentPage - 1)}> + +
    +
    + )} +

    + Page {currentPage + 1} of {numPages} +

    + {currentPage !== numPages - 1 && ( +
    +
    setCurrentPage(currentPage + 1)}> + +
    + +
    setCurrentPage(numPages - 1)}> + +
    +
    + )} + {currentPage === numPages - 1 && ( +
    + + +
    + )} +
    + ); +}; diff --git a/frontend/src/components/PasswordSetOrReset.tsx b/frontend/src/components/PasswordSetOrReset.tsx new file mode 100644 index 0000000..07749ba --- /dev/null +++ b/frontend/src/components/PasswordSetOrReset.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import {toast, ToastContainer} from 'react-toastify'; + +import Head from 'next/head'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import {useRouter} from 'next/router'; +import {getPageTitle} from '../config'; + +import {Field, Form, Formik} from 'formik'; +import FormField from '../components/FormField'; +import BaseButtons from '../components/BaseButtons'; +import BaseButton from '../components/BaseButton'; +import { passwordReset } from '../stores/authSlice'; +import {useAppDispatch} from '../stores/hooks'; + +export default function PasswordSetOrReset() { + const [loading, setLoading] = React.useState(false); + const [isInvitation, setIsInvitation] = React.useState(false); + const router = useRouter(); + const {token, invitation} = router.query; + + const notify = (type, msg) => toast(msg, {type}); + + const dispatch = useAppDispatch(); + + React.useEffect(() => { + if (invitation) { + setIsInvitation(true); + } + }, [invitation]); + + const handleSubmit = async (value) => { + setLoading(true); + if (typeof token === 'string') { + await dispatch( + passwordReset({ + token, + password: value.password, + type: isInvitation && 'invitation', + }), + ); + await router.push('/login'); + } + + setLoading(false); + }; + + return ( + <> + + {isInvitation && {getPageTitle('Set Password')}} + {!isInvitation && {getPageTitle('Reset Password')}} + + + +
    + + {isInvitation &&

    Set Password

    } + {!isInvitation &&

    Reset Password

    } +

    Enter your new password

    + + handleSubmit(values)} + > + {({errors, touched}) => ( +
    + + + + + + + + + + +
    + )} +
    +
    +
    +
    + + + ); +} diff --git a/frontend/src/components/Payment_revenues/CardPayment_revenues.tsx b/frontend/src/components/Payment_revenues/CardPayment_revenues.tsx new file mode 100644 index 0000000..8e83093 --- /dev/null +++ b/frontend/src/components/Payment_revenues/CardPayment_revenues.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payment_revenues: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPayment_revenues = ({ + payment_revenues, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && payment_revenues.map((item, index) => ( +
    • + + {item.transaction_date} + + +
      + +
      +
    +
    + +
    +
    TransactionDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.transaction_date) } +
    +
    +
    + +
    +
    AdminFeeAmount
    +
    +
    + { item.admin_fee_amount } +
    +
    +
    + +
    +
    InterestAmount
    +
    +
    + { item.interest_amount } +
    +
    +
    + +
    +
    CommissionAmount
    +
    +
    + { item.commission_amount } +
    +
    +
    + +
    +
    ExpenseAmount
    +
    +
    + { item.expense_amount } +
    +
    +
    + +
    +
    SettlementDatetime
    +
    +
    + { dataFormatter.dateTimeFormatter(item.setlement_datetime) } +
    +
    +
    + +
    +
    Branch
    +
    +
    + { dataFormatter.branchesOneListFormatter(item.branch) } +
    +
    +
    + +
    + + ))} + {!loading && payment_revenues.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardPayment_revenues; diff --git a/frontend/src/components/Payment_revenues/ListPayment_revenues.tsx b/frontend/src/components/Payment_revenues/ListPayment_revenues.tsx new file mode 100644 index 0000000..6c720c4 --- /dev/null +++ b/frontend/src/components/Payment_revenues/ListPayment_revenues.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payment_revenues: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListPayment_revenues = ({ payment_revenues, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && payment_revenues.map((item) => ( +
    + +
    + + +
    +

    TransactionDate

    +

    { dataFormatter.dateTimeFormatter(item.transaction_date) }

    +
    + +
    +

    AdminFeeAmount

    +

    { item.admin_fee_amount }

    +
    + +
    +

    InterestAmount

    +

    { item.interest_amount }

    +
    + +
    +

    CommissionAmount

    +

    { item.commission_amount }

    +
    + +
    +

    ExpenseAmount

    +

    { item.expense_amount }

    +
    + +
    +

    SettlementDatetime

    +

    { dataFormatter.dateTimeFormatter(item.setlement_datetime) }

    +
    + +
    +

    Branch

    +

    { dataFormatter.branchesOneListFormatter(item.branch) }

    +
    + + + +
    +
    +
    + ))} + {!loading && payment_revenues.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListPayment_revenues diff --git a/frontend/src/components/Payment_revenues/TablePayment_revenues.tsx b/frontend/src/components/Payment_revenues/TablePayment_revenues.tsx new file mode 100644 index 0000000..500c398 --- /dev/null +++ b/frontend/src/components/Payment_revenues/TablePayment_revenues.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/payment_revenues/payment_revenuesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configurePayment_revenuesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSamplePayment_revenues = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { payment_revenues, loading, count, notify: payment_revenuesNotify, refetch } = useAppSelector((state) => state.payment_revenues) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (payment_revenuesNotify.showNotification) { + notify(payment_revenuesNotify.typeNotification, payment_revenuesNotify.textNotification); + } + }, [payment_revenuesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `payment_revenues`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={payment_revenues ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSamplePayment_revenues diff --git a/frontend/src/components/Payment_revenues/configurePayment_revenuesCols.tsx b/frontend/src/components/Payment_revenues/configurePayment_revenuesCols.tsx new file mode 100644 index 0000000..bc9fb84 --- /dev/null +++ b/frontend/src/components/Payment_revenues/configurePayment_revenuesCols.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'transaction_date', + headerName: 'TransactionDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.transaction_date), + + }, + + { + field: 'admin_fee_amount', + headerName: 'AdminFeeAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'interest_amount', + headerName: 'InterestAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'commission_amount', + headerName: 'CommissionAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'expense_amount', + headerName: 'ExpenseAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'setlement_datetime', + headerName: 'SettlementDatetime', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.setlement_datetime), + + }, + + { + field: 'branch', + headerName: 'Branch', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('branches'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Payment_status/CardPayment_status.tsx b/frontend/src/components/Payment_status/CardPayment_status.tsx new file mode 100644 index 0000000..6e1ba7c --- /dev/null +++ b/frontend/src/components/Payment_status/CardPayment_status.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payment_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPayment_status = ({ + payment_status, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && payment_status.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    +
    CSS
    +
    +
    + { item.css } +
    +
    +
    + +
    +
    Visible
    +
    +
    + { dataFormatter.booleanFormatter(item.visible) } +
    +
    +
    + +
    + + ))} + {!loading && payment_status.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardPayment_status; diff --git a/frontend/src/components/Payment_status/ListPayment_status.tsx b/frontend/src/components/Payment_status/ListPayment_status.tsx new file mode 100644 index 0000000..6749f13 --- /dev/null +++ b/frontend/src/components/Payment_status/ListPayment_status.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payment_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListPayment_status = ({ payment_status, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && payment_status.map((item) => ( +
    + +
    + + +
    +

    Name

    +

    { item.name }

    +
    + +
    +

    CSS

    +

    { item.css }

    +
    + +
    +

    Visible

    +

    { dataFormatter.booleanFormatter(item.visible) }

    +
    + + + +
    +
    +
    + ))} + {!loading && payment_status.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListPayment_status diff --git a/frontend/src/components/Payment_status/TablePayment_status.tsx b/frontend/src/components/Payment_status/TablePayment_status.tsx new file mode 100644 index 0000000..d262753 --- /dev/null +++ b/frontend/src/components/Payment_status/TablePayment_status.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/payment_status/payment_statusSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configurePayment_statusCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSamplePayment_status = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { payment_status, loading, count, notify: payment_statusNotify, refetch } = useAppSelector((state) => state.payment_status) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (payment_statusNotify.showNotification) { + notify(payment_statusNotify.typeNotification, payment_statusNotify.textNotification); + } + }, [payment_statusNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `payment_status`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={payment_status ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSamplePayment_status diff --git a/frontend/src/components/Payment_status/configurePayment_statusCols.tsx b/frontend/src/components/Payment_status/configurePayment_statusCols.tsx new file mode 100644 index 0000000..668cda2 --- /dev/null +++ b/frontend/src/components/Payment_status/configurePayment_statusCols.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'css', + headerName: 'CSS', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'visible', + headerName: 'Visible', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Payment_transactions/CardPayment_transactions.tsx b/frontend/src/components/Payment_transactions/CardPayment_transactions.tsx new file mode 100644 index 0000000..ded5059 --- /dev/null +++ b/frontend/src/components/Payment_transactions/CardPayment_transactions.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payment_transactions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPayment_transactions = ({ + payment_transactions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && payment_transactions.map((item, index) => ( +
    • + + {item.transaction_amount} + + +
      + +
      +
    +
    + +
    +
    TransactionDatetime
    +
    +
    + { dataFormatter.dateTimeFormatter(item.transaction_datetime) } +
    +
    +
    + +
    +
    TransactionAmount
    +
    +
    + { item.transaction_amount } +
    +
    +
    + +
    +
    DeductAmount
    +
    +
    + { item.deduct_amount } +
    +
    +
    + +
    +
    InterestAmount
    +
    +
    + { item.interest_amount } +
    +
    +
    + +
    +
    CommissionAmount
    +
    +
    + { item.commission_amount } +
    +
    +
    + +
    +
    RevenueAmount
    +
    +
    + { item.revenue_amount } +
    +
    +
    + +
    +
    SettlementDatetime
    +
    +
    + { dataFormatter.dateTimeFormatter(item.setlement_datetime) } +
    +
    +
    + +
    +
    Type
    +
    +
    + { item.type } +
    +
    +
    + +
    +
    Payment
    +
    +
    + { dataFormatter.paymentsOneListFormatter(item.payment) } +
    +
    +
    + +
    + + ))} + {!loading && payment_transactions.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardPayment_transactions; diff --git a/frontend/src/components/Payment_transactions/ListPayment_transactions.tsx b/frontend/src/components/Payment_transactions/ListPayment_transactions.tsx new file mode 100644 index 0000000..fc279ee --- /dev/null +++ b/frontend/src/components/Payment_transactions/ListPayment_transactions.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payment_transactions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListPayment_transactions = ({ payment_transactions, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && payment_transactions.map((item) => ( +
    + +
    + + +
    +

    TransactionDatetime

    +

    { dataFormatter.dateTimeFormatter(item.transaction_datetime) }

    +
    + +
    +

    TransactionAmount

    +

    { item.transaction_amount }

    +
    + +
    +

    DeductAmount

    +

    { item.deduct_amount }

    +
    + +
    +

    InterestAmount

    +

    { item.interest_amount }

    +
    + +
    +

    CommissionAmount

    +

    { item.commission_amount }

    +
    + +
    +

    RevenueAmount

    +

    { item.revenue_amount }

    +
    + +
    +

    SettlementDatetime

    +

    { dataFormatter.dateTimeFormatter(item.setlement_datetime) }

    +
    + +
    +

    Type

    +

    { item.type }

    +
    + +
    +

    Payment

    +

    { dataFormatter.paymentsOneListFormatter(item.payment) }

    +
    + + + +
    +
    +
    + ))} + {!loading && payment_transactions.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListPayment_transactions diff --git a/frontend/src/components/Payment_transactions/TablePayment_transactions.tsx b/frontend/src/components/Payment_transactions/TablePayment_transactions.tsx new file mode 100644 index 0000000..666ab38 --- /dev/null +++ b/frontend/src/components/Payment_transactions/TablePayment_transactions.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/payment_transactions/payment_transactionsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configurePayment_transactionsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSamplePayment_transactions = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { payment_transactions, loading, count, notify: payment_transactionsNotify, refetch } = useAppSelector((state) => state.payment_transactions) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (payment_transactionsNotify.showNotification) { + notify(payment_transactionsNotify.typeNotification, payment_transactionsNotify.textNotification); + } + }, [payment_transactionsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `payment_transactions`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={payment_transactions ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSamplePayment_transactions diff --git a/frontend/src/components/Payment_transactions/configurePayment_transactionsCols.tsx b/frontend/src/components/Payment_transactions/configurePayment_transactionsCols.tsx new file mode 100644 index 0000000..df8f4b7 --- /dev/null +++ b/frontend/src/components/Payment_transactions/configurePayment_transactionsCols.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'transaction_datetime', + headerName: 'TransactionDatetime', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.transaction_datetime), + + }, + + { + field: 'transaction_amount', + headerName: 'TransactionAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'deduct_amount', + headerName: 'DeductAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'interest_amount', + headerName: 'InterestAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'commission_amount', + headerName: 'CommissionAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'revenue_amount', + headerName: 'RevenueAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'setlement_datetime', + headerName: 'SettlementDatetime', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.setlement_datetime), + + }, + + { + field: 'type', + headerName: 'Type', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'payment', + headerName: 'Payment', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('payments'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Payments/CardPayments.tsx b/frontend/src/components/Payments/CardPayments.tsx new file mode 100644 index 0000000..7efbd1e --- /dev/null +++ b/frontend/src/components/Payments/CardPayments.tsx @@ -0,0 +1,237 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payments: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPayments = ({ + payments, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && payments.map((item, index) => ( +
    • + + {item.remark} + + +
      + +
      +
    +
    + +
    +
    StartPaymentDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.start_payment_date) } +
    +
    +
    + +
    +
    PaymentDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.payment_date) } +
    +
    +
    + +
    +
    LastPaymentPaidDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.last_payment_paid_date) } +
    +
    +
    + +
    +
    Sort
    +
    +
    + { item.sort } +
    +
    +
    + +
    +
    DeductAmount
    +
    +
    + { item.deduct_amount } +
    +
    +
    + +
    +
    DeductPaidAmount
    +
    +
    + { item.deduct_paid_amount } +
    +
    +
    + +
    +
    Interval
    +
    +
    + { item.interval } +
    +
    +
    + +
    +
    InterestAmount
    +
    +
    + { item.interest_amount } +
    +
    +
    + +
    +
    CommissionAmount
    +
    +
    + { item.commission_amount } +
    +
    +
    + +
    +
    TotalAmount
    +
    +
    + { item.total_amount } +
    +
    +
    + +
    +
    TotalPaidAmount
    +
    +
    + { item.total_paid_amount } +
    +
    +
    + +
    +
    PenaltyAmount
    +
    +
    + { item.penalty_amount } +
    +
    +
    + +
    +
    PendingAmount
    +
    +
    + { item.pending_amount } +
    +
    +
    + +
    +
    CrossAmount
    +
    +
    + { item.cross_amount } +
    +
    +
    + +
    +
    Remark
    +
    +
    + { item.remark } +
    +
    +
    + +
    +
    Loan
    +
    +
    + { dataFormatter.loansOneListFormatter(item.loan) } +
    +
    +
    + +
    +
    Status
    +
    +
    + { dataFormatter.payment_statusOneListFormatter(item.status) } +
    +
    +
    + +
    + + ))} + {!loading && payments.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardPayments; diff --git a/frontend/src/components/Payments/ListPayments.tsx b/frontend/src/components/Payments/ListPayments.tsx new file mode 100644 index 0000000..1af321e --- /dev/null +++ b/frontend/src/components/Payments/ListPayments.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + payments: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListPayments = ({ payments, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && payments.map((item) => ( +
    + +
    + + +
    +

    StartPaymentDate

    +

    { dataFormatter.dateTimeFormatter(item.start_payment_date) }

    +
    + +
    +

    PaymentDate

    +

    { dataFormatter.dateTimeFormatter(item.payment_date) }

    +
    + +
    +

    LastPaymentPaidDate

    +

    { dataFormatter.dateTimeFormatter(item.last_payment_paid_date) }

    +
    + +
    +

    Sort

    +

    { item.sort }

    +
    + +
    +

    DeductAmount

    +

    { item.deduct_amount }

    +
    + +
    +

    DeductPaidAmount

    +

    { item.deduct_paid_amount }

    +
    + +
    +

    Interval

    +

    { item.interval }

    +
    + +
    +

    InterestAmount

    +

    { item.interest_amount }

    +
    + +
    +

    CommissionAmount

    +

    { item.commission_amount }

    +
    + +
    +

    TotalAmount

    +

    { item.total_amount }

    +
    + +
    +

    TotalPaidAmount

    +

    { item.total_paid_amount }

    +
    + +
    +

    PenaltyAmount

    +

    { item.penalty_amount }

    +
    + +
    +

    PendingAmount

    +

    { item.pending_amount }

    +
    + +
    +

    CrossAmount

    +

    { item.cross_amount }

    +
    + +
    +

    Remark

    +

    { item.remark }

    +
    + +
    +

    Loan

    +

    { dataFormatter.loansOneListFormatter(item.loan) }

    +
    + +
    +

    Status

    +

    { dataFormatter.payment_statusOneListFormatter(item.status) }

    +
    + + + +
    +
    +
    + ))} + {!loading && payments.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListPayments diff --git a/frontend/src/components/Payments/TablePayments.tsx b/frontend/src/components/Payments/TablePayments.tsx new file mode 100644 index 0000000..18d5dc8 --- /dev/null +++ b/frontend/src/components/Payments/TablePayments.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/payments/paymentsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configurePaymentsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSamplePayments = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { payments, loading, count, notify: paymentsNotify, refetch } = useAppSelector((state) => state.payments) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (paymentsNotify.showNotification) { + notify(paymentsNotify.typeNotification, paymentsNotify.textNotification); + } + }, [paymentsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `payments`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={payments ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSamplePayments diff --git a/frontend/src/components/Payments/configurePaymentsCols.tsx b/frontend/src/components/Payments/configurePaymentsCols.tsx new file mode 100644 index 0000000..624e7de --- /dev/null +++ b/frontend/src/components/Payments/configurePaymentsCols.tsx @@ -0,0 +1,307 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'start_payment_date', + headerName: 'StartPaymentDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.start_payment_date), + + }, + + { + field: 'payment_date', + headerName: 'PaymentDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.payment_date), + + }, + + { + field: 'last_payment_paid_date', + headerName: 'LastPaymentPaidDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.last_payment_paid_date), + + }, + + { + field: 'sort', + headerName: 'Sort', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'deduct_amount', + headerName: 'DeductAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'deduct_paid_amount', + headerName: 'DeductPaidAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'interval', + headerName: 'Interval', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'interest_amount', + headerName: 'InterestAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'commission_amount', + headerName: 'CommissionAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'total_amount', + headerName: 'TotalAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'total_paid_amount', + headerName: 'TotalPaidAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'penalty_amount', + headerName: 'PenaltyAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'pending_amount', + headerName: 'PendingAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'cross_amount', + headerName: 'CrossAmount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'remark', + headerName: 'Remark', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'loan', + headerName: 'Loan', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('loans'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('payment_status'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Provinces/CardProvinces.tsx b/frontend/src/components/Provinces/CardProvinces.tsx new file mode 100644 index 0000000..517b3d6 --- /dev/null +++ b/frontend/src/components/Provinces/CardProvinces.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + provinces: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardProvinces = ({ + provinces, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && provinces.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    Active
    +
    +
    + { dataFormatter.booleanFormatter(item.active) } +
    +
    +
    + +
    + + ))} + {!loading && provinces.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardProvinces; diff --git a/frontend/src/components/Provinces/ListProvinces.tsx b/frontend/src/components/Provinces/ListProvinces.tsx new file mode 100644 index 0000000..fc2c4a9 --- /dev/null +++ b/frontend/src/components/Provinces/ListProvinces.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + provinces: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListProvinces = ({ provinces, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && provinces.map((item) => ( +
    + +
    + + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    Active

    +

    { dataFormatter.booleanFormatter(item.active) }

    +
    + + + +
    +
    +
    + ))} + {!loading && provinces.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListProvinces diff --git a/frontend/src/components/Provinces/TableProvinces.tsx b/frontend/src/components/Provinces/TableProvinces.tsx new file mode 100644 index 0000000..055148a --- /dev/null +++ b/frontend/src/components/Provinces/TableProvinces.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/provinces/provincesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureProvincesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleProvinces = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { provinces, loading, count, notify: provincesNotify, refetch } = useAppSelector((state) => state.provinces) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (provincesNotify.showNotification) { + notify(provincesNotify.typeNotification, provincesNotify.textNotification); + } + }, [provincesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `provinces`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={provinces ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleProvinces diff --git a/frontend/src/components/Provinces/configureProvincesCols.tsx b/frontend/src/components/Provinces/configureProvincesCols.tsx new file mode 100644 index 0000000..d84ead4 --- /dev/null +++ b/frontend/src/components/Provinces/configureProvincesCols.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'active', + headerName: 'Active', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/RichTextField.tsx b/frontend/src/components/RichTextField.tsx new file mode 100644 index 0000000..3a1aec8 --- /dev/null +++ b/frontend/src/components/RichTextField.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useId, useState } from 'react'; +import {Editor} from "@tinymce/tinymce-react"; +import { tinyKey } from '../config' +import {useAppSelector} from "../stores/hooks"; + +export const RichTextField = ({ options, field, form, itemRef, showField }) => { + const [value, setValue] = useState(null); + const darkMode = useAppSelector((state) => state.style.darkMode); + + useEffect(() => { + if (field.value) { + setValue(field.value); + } + }, [field.value]); + + const handleChange = (value) => { + form.setFieldValue(field.name, value); + setValue(value); + }; + + return ( + + ); +}; diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx new file mode 100644 index 0000000..5e7c5de --- /dev/null +++ b/frontend/src/components/Search.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Field, Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import { useAppSelector } from '../stores/hooks'; + +const Search = () => { + const router = useRouter(); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const validateSearch = (value) => { + let error; + if (!value) { + error = 'Required'; + } else if (value.length < 2) { + error = 'Minimum length: 2 characters' + } + return error; + }; + return ( + { + router.push(`/search?query=${values.search}`); + resetForm(); + setSubmitting(false); + }} + validateOnBlur={false} + validateOnChange={false} + > + {({ errors, touched, values }) => ( +
    + + {errors.search && touched.search && values.search.length < 2 ? ( +
    {errors.search}
    + ) : null} + + )} +
    + ); +}; +export default Search; \ No newline at end of file diff --git a/frontend/src/components/SearchResults.tsx b/frontend/src/components/SearchResults.tsx new file mode 100644 index 0000000..4a417ca --- /dev/null +++ b/frontend/src/components/SearchResults.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import CardBox from './CardBox'; +import { useRouter } from 'next/router'; +import { humanize } from '../helpers/humanize'; + +const SearchResults = ({ searchResults, searchQuery }) => { + const router = useRouter(); + + return ( + <> +

    Matches with: {searchQuery}

    + {Object.keys(searchResults).map((tableName) => ( + <> +

    {humanize(tableName)}

    + +
    + + + + {searchResults[tableName].length > 0 && + Object.keys(searchResults[tableName][0]).map((key) => { + if ( + key !== 'tableName' && + key !== 'id' && + key !== 'matchAttribute' + ) { + return ( + + ); + } + return null; + })} + + + + {searchResults[tableName].map((item, index) => ( + + {Object.keys(item).map((key) => { + if ( + key !== 'tableName' && + key !== 'id' && + key !== 'matchAttribute' + ) { + return ( + + ); + } + return null; + })} + + ))} + +
    + {humanize(key)} +
    + router.push( + `/${tableName}/${tableName}-view/?id=${item['id']}`, + ) + } + > + {item[key]} +
    +
    + {!Object.keys(searchResults).length && ( +
    No data
    + )} +
    + + ))} + {!Object.keys(searchResults).length && ( +
    No matches
    + )} + + ); +}; + +export default SearchResults; diff --git a/frontend/src/components/SectionFullScreen.tsx b/frontend/src/components/SectionFullScreen.tsx new file mode 100644 index 0000000..a570d23 --- /dev/null +++ b/frontend/src/components/SectionFullScreen.tsx @@ -0,0 +1,27 @@ +import React, { ReactNode } from 'react' +import { BgKey } from '../interfaces' +import {gradientBgPurplePink, gradientBgDark, gradientBgPinkRed, gradientBgViolet} from '../colors' +import { useAppSelector } from '../stores/hooks' + +type Props = { + bg: BgKey + children?: ReactNode +} + +export default function SectionFullScreen({ bg, children }: Props) { + const darkMode = useAppSelector((state) => state.style.darkMode) + + let componentClass = 'flex min-h-screen items-center justify-center ' + + if (darkMode) { + componentClass += gradientBgDark + } else if (bg === 'violet') { + componentClass += gradientBgViolet + } else if (bg === 'purplePink') { + componentClass += gradientBgPurplePink + } else if (bg === 'pinkRed') { + componentClass += gradientBgPinkRed + } + + return
    {children}
    +} diff --git a/frontend/src/components/SectionMain.tsx b/frontend/src/components/SectionMain.tsx new file mode 100644 index 0000000..2b18097 --- /dev/null +++ b/frontend/src/components/SectionMain.tsx @@ -0,0 +1,10 @@ +import React, { ReactNode } from 'react' +import { containerMaxW } from '../config' + +type Props = { + children: ReactNode +} + +export default function SectionMain({ children }: Props) { + return
    {children}
    +} diff --git a/frontend/src/components/SectionTitle.tsx b/frontend/src/components/SectionTitle.tsx new file mode 100644 index 0000000..c441e9e --- /dev/null +++ b/frontend/src/components/SectionTitle.tsx @@ -0,0 +1,27 @@ +import React, { ReactNode } from 'react' + +type Props = { + custom?: boolean + first?: boolean + last?: boolean + children: ReactNode +} + +const SectionTitle = ({ custom = false, first = false, last = false, children }: Props) => { + let classAddon = '-my-6' + + if (first) { + classAddon = '-mb-6' + } else if (last) { + classAddon = '-mt-6' + } + + return ( +
    + {custom && children} + {!custom &&

    {children}

    } +
    + ) +} + +export default SectionTitle diff --git a/frontend/src/components/SectionTitleLineWithButton.tsx b/frontend/src/components/SectionTitleLineWithButton.tsx new file mode 100644 index 0000000..3d3c5ef --- /dev/null +++ b/frontend/src/components/SectionTitleLineWithButton.tsx @@ -0,0 +1,29 @@ +import { mdiCog } from '@mdi/js' +import React, { Children, ReactNode } from 'react' +import BaseButton from './BaseButton' +import BaseIcon from './BaseIcon' +import IconRounded from './IconRounded' +import { humanize } from '../helpers/humanize'; + +type Props = { + icon: string + title: string + main?: boolean + children?: ReactNode +} + +export default function SectionTitleLineWithButton({ icon, title, main = false, children }: Props) { + const hasChildren = !!Children.count(children) + + return ( +
    +
    + {icon && main && } + {icon && !main && } +

    {humanize(title)}

    +
    + {children} + {!hasChildren && } +
    + ) +} diff --git a/frontend/src/components/SelectField.tsx b/frontend/src/components/SelectField.tsx new file mode 100644 index 0000000..cab0629 --- /dev/null +++ b/frontend/src/components/SelectField.tsx @@ -0,0 +1,53 @@ +import React, {useEffect, useId, useState} from 'react'; +import { AsyncPaginate } from 'react-select-async-paginate'; +import axios from 'axios' + +export const SelectField = ({ options, field, form, itemRef, showField, disabled }) => { + + const [value, setValue] = useState(null) + const PAGE_SIZE = 100; + + useEffect(() => { + if(options?.id && field?.value?.id) { + setValue({value: field.value?.id, label: field.value[showField]}) + form.setFieldValue(field.name, field.value?.id); + } else if (!field.value) { + setValue(null); + } + }, [options?.id, field?.value?.id, field?.value]) + + const mapResponseToValuesAndLabels = (data) => ({ + value: data.id, + label: data.label, + }) + const handleChange = (option) => { + form.setFieldValue(field.name, option?.value || null) + setValue(option) + } + + async function callApi(inputValue: string, loadedOptions: any[]) { + const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${loadedOptions.length}${inputValue ? `&query=${inputValue}` : ''}`; + const { data } = await axios(path); + return { + options: data.map(mapResponseToValuesAndLabels), + hasMore: data.length === PAGE_SIZE, + } + } + return ( + 'px-1 py-2', + }} + classNamePrefix='react-select' + instanceId={useId()} + value={value} + debounceTimeout={1000} + loadOptions={callApi} + onChange={handleChange} + defaultOptions + isDisabled={disabled} + isClearable + /> + ) +} + diff --git a/frontend/src/components/Sexes/CardSexes.tsx b/frontend/src/components/Sexes/CardSexes.tsx new file mode 100644 index 0000000..94d15b0 --- /dev/null +++ b/frontend/src/components/Sexes/CardSexes.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + sexes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardSexes = ({ + sexes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && sexes.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    + + ))} + {!loading && sexes.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardSexes; diff --git a/frontend/src/components/Sexes/ListSexes.tsx b/frontend/src/components/Sexes/ListSexes.tsx new file mode 100644 index 0000000..99977ea --- /dev/null +++ b/frontend/src/components/Sexes/ListSexes.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + sexes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListSexes = ({ sexes, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && sexes.map((item) => ( +
    + +
    + + +
    +

    Name

    +

    { item.name }

    +
    + + + +
    +
    +
    + ))} + {!loading && sexes.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListSexes diff --git a/frontend/src/components/Sexes/TableSexes.tsx b/frontend/src/components/Sexes/TableSexes.tsx new file mode 100644 index 0000000..5557865 --- /dev/null +++ b/frontend/src/components/Sexes/TableSexes.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/sexes/sexesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureSexesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleSexes = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { sexes, loading, count, notify: sexesNotify, refetch } = useAppSelector((state) => state.sexes) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (sexesNotify.showNotification) { + notify(sexesNotify.typeNotification, sexesNotify.textNotification); + } + }, [sexesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `sexes`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={sexes ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleSexes diff --git a/frontend/src/components/Sexes/configureSexesCols.tsx b/frontend/src/components/Sexes/configureSexesCols.tsx new file mode 100644 index 0000000..e2b0080 --- /dev/null +++ b/frontend/src/components/Sexes/configureSexesCols.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Shareholders/CardShareholders.tsx b/frontend/src/components/Shareholders/CardShareholders.tsx new file mode 100644 index 0000000..64377c0 --- /dev/null +++ b/frontend/src/components/Shareholders/CardShareholders.tsx @@ -0,0 +1,192 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + shareholders: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardShareholders = ({ + shareholders, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && shareholders.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    EarnRate
    +
    +
    + { item.earn_rate } +
    +
    +
    + +
    +
    DateofBirth
    +
    +
    + { dataFormatter.dateTimeFormatter(item.date_of_birth) } +
    +
    +
    + +
    +
    PhoneNumber
    +
    +
    + { item.phone_number } +
    +
    +
    + +
    +
    StartWorkDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.start_work_date) } +
    +
    +
    + +
    +
    BornPlace
    +
    +
    + { item.born_place } +
    +
    +
    + +
    +
    DocumentType
    +
    +
    + { item.document_type } +
    +
    +
    + +
    +
    DocumentNumber
    +
    +
    + { item.document_number } +
    +
    +
    + +
    +
    EmergencyNumber
    +
    +
    + { item.emergency_number } +
    +
    +
    + +
    +
    CurrentPlace
    +
    +
    + { item.current_place } +
    +
    +
    + +
    +
    Sex
    +
    +
    + { item.sex } +
    +
    +
    + +
    + + ))} + {!loading && shareholders.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardShareholders; diff --git a/frontend/src/components/Shareholders/ListShareholders.tsx b/frontend/src/components/Shareholders/ListShareholders.tsx new file mode 100644 index 0000000..f9e7f51 --- /dev/null +++ b/frontend/src/components/Shareholders/ListShareholders.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + shareholders: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListShareholders = ({ shareholders, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && shareholders.map((item) => ( +
    + +
    + + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    EarnRate

    +

    { item.earn_rate }

    +
    + +
    +

    DateofBirth

    +

    { dataFormatter.dateTimeFormatter(item.date_of_birth) }

    +
    + +
    +

    PhoneNumber

    +

    { item.phone_number }

    +
    + +
    +

    StartWorkDate

    +

    { dataFormatter.dateTimeFormatter(item.start_work_date) }

    +
    + +
    +

    BornPlace

    +

    { item.born_place }

    +
    + +
    +

    DocumentType

    +

    { item.document_type }

    +
    + +
    +

    DocumentNumber

    +

    { item.document_number }

    +
    + +
    +

    EmergencyNumber

    +

    { item.emergency_number }

    +
    + +
    +

    CurrentPlace

    +

    { item.current_place }

    +
    + +
    +

    Sex

    +

    { item.sex }

    +
    + + + +
    +
    +
    + ))} + {!loading && shareholders.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListShareholders diff --git a/frontend/src/components/Shareholders/TableShareholders.tsx b/frontend/src/components/Shareholders/TableShareholders.tsx new file mode 100644 index 0000000..2dc3328 --- /dev/null +++ b/frontend/src/components/Shareholders/TableShareholders.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/shareholders/shareholdersSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureShareholdersCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleShareholders = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { shareholders, loading, count, notify: shareholdersNotify, refetch } = useAppSelector((state) => state.shareholders) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (shareholdersNotify.showNotification) { + notify(shareholdersNotify.typeNotification, shareholdersNotify.textNotification); + } + }, [shareholdersNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `shareholders`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={shareholders ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleShareholders diff --git a/frontend/src/components/Shareholders/configureShareholdersCols.tsx b/frontend/src/components/Shareholders/configureShareholdersCols.tsx new file mode 100644 index 0000000..a6aee1c --- /dev/null +++ b/frontend/src/components/Shareholders/configureShareholdersCols.tsx @@ -0,0 +1,215 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'earn_rate', + headerName: 'EarnRate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'date_of_birth', + headerName: 'DateofBirth', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.date_of_birth), + + }, + + { + field: 'phone_number', + headerName: 'PhoneNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'start_work_date', + headerName: 'StartWorkDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.start_work_date), + + }, + + { + field: 'born_place', + headerName: 'BornPlace', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'document_type', + headerName: 'DocumentType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'document_number', + headerName: 'DocumentNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'emergency_number', + headerName: 'EmergencyNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'current_place', + headerName: 'CurrentPlace', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'sex', + headerName: 'Sex', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Staff_status/CardStaff_status.tsx b/frontend/src/components/Staff_status/CardStaff_status.tsx new file mode 100644 index 0000000..1f14c13 --- /dev/null +++ b/frontend/src/components/Staff_status/CardStaff_status.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + staff_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardStaff_status = ({ + staff_status, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && staff_status.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    +
    CSS
    +
    +
    + { item.css } +
    +
    +
    + +
    + + ))} + {!loading && staff_status.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardStaff_status; diff --git a/frontend/src/components/Staff_status/ListStaff_status.tsx b/frontend/src/components/Staff_status/ListStaff_status.tsx new file mode 100644 index 0000000..fc07719 --- /dev/null +++ b/frontend/src/components/Staff_status/ListStaff_status.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + staff_status: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListStaff_status = ({ staff_status, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && staff_status.map((item) => ( +
    + +
    + + +
    +

    Name

    +

    { item.name }

    +
    + +
    +

    CSS

    +

    { item.css }

    +
    + + + +
    +
    +
    + ))} + {!loading && staff_status.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListStaff_status diff --git a/frontend/src/components/Staff_status/TableStaff_status.tsx b/frontend/src/components/Staff_status/TableStaff_status.tsx new file mode 100644 index 0000000..54ae9bb --- /dev/null +++ b/frontend/src/components/Staff_status/TableStaff_status.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/staff_status/staff_statusSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureStaff_statusCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleStaff_status = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { staff_status, loading, count, notify: staff_statusNotify, refetch } = useAppSelector((state) => state.staff_status) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (staff_statusNotify.showNotification) { + notify(staff_statusNotify.typeNotification, staff_statusNotify.textNotification); + } + }, [staff_statusNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `staff_status`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={staff_status ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleStaff_status diff --git a/frontend/src/components/Staff_status/configureStaff_statusCols.tsx b/frontend/src/components/Staff_status/configureStaff_statusCols.tsx new file mode 100644 index 0000000..b4fe125 --- /dev/null +++ b/frontend/src/components/Staff_status/configureStaff_statusCols.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'css', + headerName: 'CSS', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Staffs/CardStaffs.tsx b/frontend/src/components/Staffs/CardStaffs.tsx new file mode 100644 index 0000000..3b3bc80 --- /dev/null +++ b/frontend/src/components/Staffs/CardStaffs.tsx @@ -0,0 +1,201 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + staffs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardStaffs = ({ + staffs, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && staffs.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    DateofBirth
    +
    +
    + { dataFormatter.dateTimeFormatter(item.date_of_birth) } +
    +
    +
    + +
    +
    PhoneNumber
    +
    +
    + { item.phone_number } +
    +
    +
    + +
    +
    StartWorkDate
    +
    +
    + { dataFormatter.dateTimeFormatter(item.start_work_date) } +
    +
    +
    + +
    +
    BornPlace
    +
    +
    + { item.born_place } +
    +
    +
    + +
    +
    DocumentType
    +
    +
    + { item.document_type } +
    +
    +
    + +
    +
    DocumentNumber
    +
    +
    + { item.document_number } +
    +
    +
    + +
    +
    EmergencyNumber
    +
    +
    + { item.emergency_number } +
    +
    +
    + +
    +
    CurrentPlace
    +
    +
    + { item.current_place } +
    +
    +
    + +
    +
    Status
    +
    +
    + { dataFormatter.staff_statusOneListFormatter(item.status) } +
    +
    +
    + +
    +
    Sex
    +
    +
    + { item.sex } +
    +
    +
    + +
    +
    Branch
    +
    +
    + { dataFormatter.branchesOneListFormatter(item.branch) } +
    +
    +
    + +
    + + ))} + {!loading && staffs.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardStaffs; diff --git a/frontend/src/components/Staffs/ListStaffs.tsx b/frontend/src/components/Staffs/ListStaffs.tsx new file mode 100644 index 0000000..e145b4d --- /dev/null +++ b/frontend/src/components/Staffs/ListStaffs.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + staffs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListStaffs = ({ staffs, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && staffs.map((item) => ( +
    + +
    + + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    DateofBirth

    +

    { dataFormatter.dateTimeFormatter(item.date_of_birth) }

    +
    + +
    +

    PhoneNumber

    +

    { item.phone_number }

    +
    + +
    +

    StartWorkDate

    +

    { dataFormatter.dateTimeFormatter(item.start_work_date) }

    +
    + +
    +

    BornPlace

    +

    { item.born_place }

    +
    + +
    +

    DocumentType

    +

    { item.document_type }

    +
    + +
    +

    DocumentNumber

    +

    { item.document_number }

    +
    + +
    +

    EmergencyNumber

    +

    { item.emergency_number }

    +
    + +
    +

    CurrentPlace

    +

    { item.current_place }

    +
    + +
    +

    Status

    +

    { dataFormatter.staff_statusOneListFormatter(item.status) }

    +
    + +
    +

    Sex

    +

    { item.sex }

    +
    + +
    +

    Branch

    +

    { dataFormatter.branchesOneListFormatter(item.branch) }

    +
    + + + +
    +
    +
    + ))} + {!loading && staffs.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListStaffs diff --git a/frontend/src/components/Staffs/TableStaffs.tsx b/frontend/src/components/Staffs/TableStaffs.tsx new file mode 100644 index 0000000..352aa55 --- /dev/null +++ b/frontend/src/components/Staffs/TableStaffs.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/staffs/staffsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureStaffsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleStaffs = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { staffs, loading, count, notify: staffsNotify, refetch } = useAppSelector((state) => state.staffs) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (staffsNotify.showNotification) { + notify(staffsNotify.typeNotification, staffsNotify.textNotification); + } + }, [staffsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `staffs`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={staffs ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleStaffs diff --git a/frontend/src/components/Staffs/configureStaffsCols.tsx b/frontend/src/components/Staffs/configureStaffsCols.tsx new file mode 100644 index 0000000..bbc0e37 --- /dev/null +++ b/frontend/src/components/Staffs/configureStaffsCols.tsx @@ -0,0 +1,241 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'date_of_birth', + headerName: 'DateofBirth', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.date_of_birth), + + }, + + { + field: 'phone_number', + headerName: 'PhoneNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'start_work_date', + headerName: 'StartWorkDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.start_work_date), + + }, + + { + field: 'born_place', + headerName: 'BornPlace', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'document_type', + headerName: 'DocumentType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'document_number', + headerName: 'DocumentNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'emergency_number', + headerName: 'EmergencyNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'current_place', + headerName: 'CurrentPlace', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('staff_status'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'sex', + headerName: 'Sex', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'branch', + headerName: 'Branch', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('branches'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/SwitchField.tsx b/frontend/src/components/SwitchField.tsx new file mode 100644 index 0000000..a3a66e8 --- /dev/null +++ b/frontend/src/components/SwitchField.tsx @@ -0,0 +1,19 @@ +import React, { useEffect, useId, useState } from 'react'; +import Switch from "react-switch"; + +export const SwitchField = ({ + field, + form, + disabled + }) => { + const handleChange = (data: any) => { + form.setFieldValue( + field.name, + data, + ); + }; + + return ( + + ); +}; diff --git a/frontend/src/components/TableSampleClients.tsx b/frontend/src/components/TableSampleClients.tsx new file mode 100644 index 0000000..3fb3411 --- /dev/null +++ b/frontend/src/components/TableSampleClients.tsx @@ -0,0 +1,141 @@ +import { mdiEye, mdiTrashCan } from '@mdi/js' +import React, { useState } from 'react' +import { useSampleClients } from '../hooks/sampleData' +import { Client } from '../interfaces' +import BaseButton from './BaseButton' +import BaseButtons from './BaseButtons' +import CardBoxModal from './CardBoxModal' +import UserAvatar from './UserAvatar' + +const TableSampleClients = () => { + const { clients } = useSampleClients() + + const perPage = 5 + + const [currentPage, setCurrentPage] = useState(0) + + const clientsPaginated = clients.slice(perPage * currentPage, perPage * (currentPage + 1)) + + const numPages = clients.length / perPage + + const pagesList = [] + + for (let i = 0; i < numPages; i++) { + pagesList.push(i) + } + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + return ( + <> + +

    + Lorem ipsum dolor sit amet adipiscing elit +

    +

    This is sample modal

    +
    + + +

    + Lorem ipsum dolor sit amet adipiscing elit +

    +

    This is sample modal

    +
    + + + + + + + + + + + + + {clientsPaginated.map((client: Client) => ( + + + + + + + + + + ))} + +
    + NameCompanyCityProgressCreated +
    + + {client.name}{client.company}{client.city} + + {client.progress} + + + {client.created} + + + setIsModalInfoActive(true)} + small + /> + setIsModalTrashActive(true)} + small + /> + +
    +
    +
    + + {pagesList.map((page) => ( + setCurrentPage(page)} + /> + ))} + + + Page {currentPage + 1} of {numPages} + +
    +
    + + ) +} + +export default TableSampleClients diff --git a/frontend/src/components/Uploaders/FilesUploader.js b/frontend/src/components/Uploaders/FilesUploader.js new file mode 100644 index 0000000..26a4b1a --- /dev/null +++ b/frontend/src/components/Uploaders/FilesUploader.js @@ -0,0 +1,150 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from "prop-types"; +import FileUploader from "components/FormItems/uploaders/UploadService"; +import Errors from "../../../components/FormItems/error/errors"; + +const FilesUploader = (props) => { + const { + value, + onChange, + schema, + path, + max, + readonly + } = props; + + const [loading, setLoading] = useState(false); + const inputElement = useRef(null); + + const valuesArr = () => { + if (!value) { + return []; + } + return Array.isArray(value) ? value : [value]; + }; + + const fileList = () => { + return valuesArr().map((item) => ({ + uid: item.id || undefined, + name: item.name, + status: 'done', + url: item.publicUrl, + })); + }; + + const handleRemove = (id) => { + onChange(valuesArr().filter((item) => item.id !== id)); + }; + + const handleChange = async (event) => { + try { + const files = event.target.files; + + if (!files || !files.length) { + return; + } + + let file = files[0]; + + FileUploader.validate(file, schema); + + setLoading(true) + + file = await FileUploader.upload( + path, + file, + schema, + ); + + inputElement.current.value = ''; + setLoading(false); + onChange([...valuesArr(), file]); + } catch (error) { + inputElement.current.value = ''; + console.log('error', error); + setLoading(false); + Errors.showMessage(error); + } + }; + + const formats = () => { + if (schema && schema.formats) { + return schema.formats + .map((format) => `.${format}`) + .join(','); + } + return undefined; + }; + + const uploadButton = ( + + ); + + return ( +
    + {readonly || (max && fileList().length >= max) + ? null + : uploadButton} + + {valuesArr() && valuesArr().length ? ( +
    + {valuesArr().map((item) => { + return ( +
    + + + {item.name} + + + {!readonly && ( + + )} +
    + ); + })} +
    + ) : null} +
    + ) +} + +FilesUploader.propTypes = { + readonly: PropTypes.bool, + path: PropTypes.string, + max: PropTypes.number, + schema: PropTypes.shape({ + image: PropTypes.bool, + size: PropTypes.number, + formats: PropTypes.arrayOf(PropTypes.string), + }), + value: PropTypes.any, + onChange: PropTypes.func, +}; + +export default FilesUploader; diff --git a/frontend/src/components/Uploaders/ImagesUploader.js b/frontend/src/components/Uploaders/ImagesUploader.js new file mode 100644 index 0000000..725abe7 --- /dev/null +++ b/frontend/src/components/Uploaders/ImagesUploader.js @@ -0,0 +1,237 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import Button from '@mui/material/Button'; +import CloseIcon from '@mui/icons-material/Close'; +import SearchIcon from '@mui/icons-material/Search'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import Dialog from '@mui/material/Dialog'; +import FileUploader from 'components/FormItems/uploaders/UploadService'; +import Errors from '../../../components/FormItems/error/errors'; +import {makeStyles} from "@mui/styles"; + +const useStyles = makeStyles({ + actionButtonsWrapper: { + position: 'relative', + }, + previewContent: { + padding: '0px !important', + }, + imageItem: { + '&.MuiGrid-root': { + margin: 10, + boxShadow: '2px 2px 8px 0 rgb(0 0 0 / 40%)', + borderRadius: 10, + }, + height: '100px', + }, + actionButtons: { + position: 'absolute', + bottom: 5, + right: 4, + }, + previewContainer: { + '& button': { + position: 'absolute', + top: 10, + right: 10, + '& svg': { + height: 50, + width: 50, + fill: '#FFF', + stroke: '#909090', + strokeWidth: 0.5, + }, + }, + }, + button: { + padding: '0px !important', + minWidth: '45px !important', + '& svg': { + height: 36, + width: 36, + fill: '#FFF', + stroke: '#909090', + strokeWidth: 0.5, + }, + } +}); + +const ImagesUploader = (props) => { + const classes = useStyles(); + const { value, onChange, schema, path, max, readonly, name } = props; + + const [loading, setLoading] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const [imageMeta, setImageMeta] = useState({ + imageSrc: null, + imageAlt: null, + }); + const inputElement = useRef(null); + + const valuesArr = () => { + if (!value) { + return []; + } + return Array.isArray(value) ? value : [value]; + }; + + const fileList = () => { + return valuesArr().map((item) => ({ + uid: item.id || undefined, + name: item.name, + status: 'done', + url: item.publicUrl, + })); + }; + + const handleRemove = (id) => { + onChange(valuesArr().filter((item) => item.id !== id)); + }; + + const handleChange = async (event) => { + try { + const files = event.target.files; + + if (!files || !files.length) { + return; + } + + let file = files[0]; + + FileUploader.validate(file, schema); + + setLoading(true); + + file = await FileUploader.upload(path, file, schema); + + inputElement.current.value = ''; + setLoading(false); + onChange([...valuesArr(), file]); + } catch (error) { + inputElement.current.value = ''; + console.log('error', error); + setLoading(false); + Errors.showMessage(error); + } + }; + + const doPreviewImage = (image) => { + setImageMeta({ + imageSrc: image.publicUrl, + imageAlt: image.name, + }); + setShowPreview(true); + }; + + const doCloseImageModal = () => { + setImageMeta({ + imageSrc: null, + imageAlt: null, + }); + setShowPreview(false); + }; + + const uploadButton = ( + + + + ); + + return ( + + {readonly || (max && fileList().length >= max) ? null : uploadButton} + + {valuesArr() && valuesArr().length ? ( + + {valuesArr().map((item) => { + return ( + + {item.name} + +
    +
    + + {!readonly && ( + + )} +
    +
    +
    + ); + })} +
    + ) : null} + + + {imageMeta.imageAlt} + +
    + ); +}; + +ImagesUploader.propTypes = { + readonly: PropTypes.bool, + path: PropTypes.string, + max: PropTypes.number, + schema: PropTypes.shape({ + image: PropTypes.bool, + size: PropTypes.number, + formats: PropTypes.arrayOf(PropTypes.string), + }), + value: PropTypes.any, + onChange: PropTypes.func, + name: PropTypes.string, +}; + +export default ImagesUploader; diff --git a/frontend/src/components/Uploaders/UploadService.js b/frontend/src/components/Uploaders/UploadService.js new file mode 100644 index 0000000..33620a6 --- /dev/null +++ b/frontend/src/components/Uploaders/UploadService.js @@ -0,0 +1,79 @@ +import { v4 as uuidv4 } from 'uuid'; +import Axios from "axios"; +import {baseURLApi} from "../../config"; + +function extractExtensionFrom(filename) { + if (!filename) { + return null; + } + + const regex = /(?:\.([^.]+))?$/; + return regex.exec(filename)[1]; +} + +export default class FileUploader { + static validate(file, schema) { + if (!schema) { + return; + } + + if (schema.image) { + if (!file.type.startsWith("image")) { + throw new Error("You must upload an image"); + } + } + + if (schema.size && file.size > schema.size) { + throw new Error("File is too big."); + } + + const extension = extractExtensionFrom(file.name); + + if (schema.formats && !schema.formats.includes(extension)) { + throw new Error("Invalid format"); + } + } + + static async upload(path, file, schema) { + try { + this.validate(file, schema); + } catch (error) { + return Promise.reject(error); + } + + const extension = extractExtensionFrom(file.name); + const id = uuidv4(); + const filename = `${id}.${extension}`; + const privateUrl = `${path}/${filename}`; + + const publicUrl = await this.uploadToServer(file, path, filename); + + return { + id: id, + name: file.name, + sizeInBytes: file.size, + privateUrl, + publicUrl, + new: true, + }; + } + + static async uploadToServer(file, path, filename) { + const formData = new FormData(); + formData.append("file", file); + formData.append("filename", filename); + const uri = `/file/upload/${path}`; + await Axios.post(uri, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + const privateUrl = `${path}/${filename}`; + + console.log("process.env.NODE_ENV in uploadToServer function", process.env.NODE_ENV); + console.log("baseURLApi in uploadToServer function", baseURLApi); + + return `${baseURLApi}/file/download?privateUrl=${privateUrl}`; + } +} diff --git a/frontend/src/components/Urls/CardUrls.tsx b/frontend/src/components/Urls/CardUrls.tsx new file mode 100644 index 0000000..cc5cbf1 --- /dev/null +++ b/frontend/src/components/Urls/CardUrls.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + urls: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardUrls = ({ + urls, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && urls.map((item, index) => ( +
    • + + {item.uri} + + +
      + +
      +
    +
    + +
    +
    Method
    +
    +
    + { item.method } +
    +
    +
    + +
    +
    URI
    +
    +
    + { item.uri } +
    +
    +
    + +
    +
    RouteName
    +
    +
    + { item.route_name } +
    +
    +
    + +
    +
    Active
    +
    +
    + { dataFormatter.booleanFormatter(item.acitve) } +
    +
    +
    + +
    +
    IsMenu
    +
    +
    + { dataFormatter.booleanFormatter(item.is_menu) } +
    +
    +
    + +
    + + ))} + {!loading && urls.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardUrls; diff --git a/frontend/src/components/Urls/ListUrls.tsx b/frontend/src/components/Urls/ListUrls.tsx new file mode 100644 index 0000000..855965f --- /dev/null +++ b/frontend/src/components/Urls/ListUrls.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + urls: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListUrls = ({ urls, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && urls.map((item) => ( +
    + +
    + + +
    +

    Method

    +

    { item.method }

    +
    + +
    +

    URI

    +

    { item.uri }

    +
    + +
    +

    RouteName

    +

    { item.route_name }

    +
    + +
    +

    Active

    +

    { dataFormatter.booleanFormatter(item.acitve) }

    +
    + +
    +

    IsMenu

    +

    { dataFormatter.booleanFormatter(item.is_menu) }

    +
    + + + +
    +
    +
    + ))} + {!loading && urls.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListUrls diff --git a/frontend/src/components/Urls/TableUrls.tsx b/frontend/src/components/Urls/TableUrls.tsx new file mode 100644 index 0000000..98eba2a --- /dev/null +++ b/frontend/src/components/Urls/TableUrls.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/urls/urlsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureUrlsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleUrls = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { urls, loading, count, notify: urlsNotify, refetch } = useAppSelector((state) => state.urls) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (urlsNotify.showNotification) { + notify(urlsNotify.typeNotification, urlsNotify.textNotification); + } + }, [urlsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `urls`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={urls ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleUrls diff --git a/frontend/src/components/Urls/configureUrlsCols.tsx b/frontend/src/components/Urls/configureUrlsCols.tsx new file mode 100644 index 0000000..9a23672 --- /dev/null +++ b/frontend/src/components/Urls/configureUrlsCols.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'method', + headerName: 'Method', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'uri', + headerName: 'URI', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'route_name', + headerName: 'RouteName', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'acitve', + headerName: 'Active', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'is_menu', + headerName: 'IsMenu', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/UserAvatar.tsx b/frontend/src/components/UserAvatar.tsx new file mode 100644 index 0000000..257ebdb --- /dev/null +++ b/frontend/src/components/UserAvatar.tsx @@ -0,0 +1,46 @@ +/* eslint-disable @next/next/no-img-element */ +// Why disabled: +// avatars.dicebear.com provides svg avatars +// next/image needs dangerouslyAllowSVG option for that + +import React, { ReactNode } from 'react'; +import BaseIcon from "./BaseIcon"; +import {mdiAccountCircleOutline} from "@mdi/js"; + +type Props = { + username: string; + avatar?: string | null; + image?: object | null; + api?: string; + className?: string; + children?: ReactNode; +}; + +export default function UserAvatar({ + username, + image, + avatar, + className = '', + children, + }: Props) { + + const avatarImage = (image && image[0]) ? `${image[0].publicUrl}` : '#'; + + return ( +
    + {avatarImage === "#" + ? + : {username} + } + {children} +
    + ); +} diff --git a/frontend/src/components/UserAvatarCurrentUser.tsx b/frontend/src/components/UserAvatarCurrentUser.tsx new file mode 100644 index 0000000..7bfc768 --- /dev/null +++ b/frontend/src/components/UserAvatarCurrentUser.tsx @@ -0,0 +1,43 @@ +import React, {ReactNode, useEffect, useState} from 'react'; +import { useAppSelector } from '../stores/hooks'; +import UserAvatar from './UserAvatar'; + +type Props = { + className?: string; + children?: ReactNode; +}; + +export default function UserAvatarCurrentUser({ + className = '', + children, + }: Props) { + const userName = useAppSelector((state) => state.main.userName); + const userAvatar = useAppSelector((state) => state.main.userAvatar); + const { currentUser, isFetching, token } = useAppSelector( + (state) => state.auth, + ); + const { users, loading } = useAppSelector((state) => state.users); + + const [ avatar, setAvatar ] = useState(null) + + useEffect(() => { + currentUserAvatarCheck() + }, []); + + useEffect(() => { + currentUserAvatarCheck() + }, [currentUser?.id, users]); + + const currentUserAvatarCheck = () => { + if (currentUser?.id) { + const image = currentUser?.avatar; + setAvatar(image); + } + } + + return ( + + {children} + + ); +} diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx new file mode 100644 index 0000000..562db0d --- /dev/null +++ b/frontend/src/components/UserCard.tsx @@ -0,0 +1,48 @@ +import { Field, Form, Formik } from 'formik' +import { useAppSelector } from '../stores/hooks' +import CardBox from './CardBox' +import FormCheckRadio from './FormCheckRadio' +import UserAvatarCurrentUser from './UserAvatarCurrentUser' + +type Props = { + className?: string +} + +const UserCard = ({ className }: Props) => { + const userName = useAppSelector((state) => state.main.userName) + + return ( + +
    + +
    +
    + alert(JSON.stringify(values, null, 2))} + > +
    + + + +
    +
    +
    +

    + Howdy, {userName}! +

    +

    + Last login 12 mins ago from 127.0.0.1 +

    +
    + Verified +
    +
    +
    +
    + ) +} + +export default UserCard diff --git a/frontend/src/components/User_has_menu/CardUser_has_menu.tsx b/frontend/src/components/User_has_menu/CardUser_has_menu.tsx new file mode 100644 index 0000000..8bada5a --- /dev/null +++ b/frontend/src/components/User_has_menu/CardUser_has_menu.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + user_has_menu: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardUser_has_menu = ({ + user_has_menu, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && user_has_menu.map((item, index) => ( +
    • + + {item.status} + + +
      + +
      +
    +
    + +
    +
    User
    +
    +
    + { dataFormatter.usersOneListFormatter(item.user) } +
    +
    +
    + +
    +
    Menu
    +
    +
    + { dataFormatter.menusOneListFormatter(item.menu) } +
    +
    +
    + +
    +
    Status
    +
    +
    + { item.status } +
    +
    +
    + +
    + + ))} + {!loading && user_has_menu.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardUser_has_menu; diff --git a/frontend/src/components/User_has_menu/ListUser_has_menu.tsx b/frontend/src/components/User_has_menu/ListUser_has_menu.tsx new file mode 100644 index 0000000..8690bb1 --- /dev/null +++ b/frontend/src/components/User_has_menu/ListUser_has_menu.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + user_has_menu: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListUser_has_menu = ({ user_has_menu, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && user_has_menu.map((item) => ( +
    + +
    + + +
    +

    User

    +

    { dataFormatter.usersOneListFormatter(item.user) }

    +
    + +
    +

    Menu

    +

    { dataFormatter.menusOneListFormatter(item.menu) }

    +
    + +
    +

    Status

    +

    { item.status }

    +
    + + + +
    +
    +
    + ))} + {!loading && user_has_menu.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListUser_has_menu diff --git a/frontend/src/components/User_has_menu/TableUser_has_menu.tsx b/frontend/src/components/User_has_menu/TableUser_has_menu.tsx new file mode 100644 index 0000000..851999e --- /dev/null +++ b/frontend/src/components/User_has_menu/TableUser_has_menu.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/user_has_menu/user_has_menuSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureUser_has_menuCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleUser_has_menu = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { user_has_menu, loading, count, notify: user_has_menuNotify, refetch } = useAppSelector((state) => state.user_has_menu) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (user_has_menuNotify.showNotification) { + notify(user_has_menuNotify.typeNotification, user_has_menuNotify.textNotification); + } + }, [user_has_menuNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `user_has_menu`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={user_has_menu ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleUser_has_menu diff --git a/frontend/src/components/User_has_menu/configureUser_has_menuCols.tsx b/frontend/src/components/User_has_menu/configureUser_has_menuCols.tsx new file mode 100644 index 0000000..e8fb4ca --- /dev/null +++ b/frontend/src/components/User_has_menu/configureUser_has_menuCols.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'menu', + headerName: 'Menu', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('menus'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/User_type_urls/CardUser_type_urls.tsx b/frontend/src/components/User_type_urls/CardUser_type_urls.tsx new file mode 100644 index 0000000..287069c --- /dev/null +++ b/frontend/src/components/User_type_urls/CardUser_type_urls.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + user_type_urls: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardUser_type_urls = ({ + user_type_urls, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && user_type_urls.map((item, index) => ( +
    • + + {item.user_type} + + +
      + +
      +
    +
    + +
    +
    UserType
    +
    +
    + { dataFormatter.user_typesOneListFormatter(item.user_type) } +
    +
    +
    + +
    +
    URL
    +
    +
    + { dataFormatter.urlsOneListFormatter(item.url) } +
    +
    +
    + +
    + + ))} + {!loading && user_type_urls.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardUser_type_urls; diff --git a/frontend/src/components/User_type_urls/ListUser_type_urls.tsx b/frontend/src/components/User_type_urls/ListUser_type_urls.tsx new file mode 100644 index 0000000..4850671 --- /dev/null +++ b/frontend/src/components/User_type_urls/ListUser_type_urls.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + user_type_urls: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListUser_type_urls = ({ user_type_urls, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && user_type_urls.map((item) => ( +
    + +
    + + +
    +

    UserType

    +

    { dataFormatter.user_typesOneListFormatter(item.user_type) }

    +
    + +
    +

    URL

    +

    { dataFormatter.urlsOneListFormatter(item.url) }

    +
    + + + +
    +
    +
    + ))} + {!loading && user_type_urls.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListUser_type_urls diff --git a/frontend/src/components/User_type_urls/TableUser_type_urls.tsx b/frontend/src/components/User_type_urls/TableUser_type_urls.tsx new file mode 100644 index 0000000..8e559f3 --- /dev/null +++ b/frontend/src/components/User_type_urls/TableUser_type_urls.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/user_type_urls/user_type_urlsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureUser_type_urlsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleUser_type_urls = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { user_type_urls, loading, count, notify: user_type_urlsNotify, refetch } = useAppSelector((state) => state.user_type_urls) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (user_type_urlsNotify.showNotification) { + notify(user_type_urlsNotify.typeNotification, user_type_urlsNotify.textNotification); + } + }, [user_type_urlsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `user_type_urls`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={user_type_urls ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleUser_type_urls diff --git a/frontend/src/components/User_type_urls/configureUser_type_urlsCols.tsx b/frontend/src/components/User_type_urls/configureUser_type_urlsCols.tsx new file mode 100644 index 0000000..4b3d0fb --- /dev/null +++ b/frontend/src/components/User_type_urls/configureUser_type_urlsCols.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'user_type', + headerName: 'UserType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('user_types'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'url', + headerName: 'URL', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('urls'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/User_types/CardUser_types.tsx b/frontend/src/components/User_types/CardUser_types.tsx new file mode 100644 index 0000000..c63591e --- /dev/null +++ b/frontend/src/components/User_types/CardUser_types.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + user_types: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardUser_types = ({ + user_types, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && user_types.map((item, index) => ( +
    • + + {item.name} + + +
      + +
      +
    +
    + +
    +
    IsAdmin
    +
    +
    + { dataFormatter.booleanFormatter(item.is_admin) } +
    +
    +
    + +
    +
    Name
    +
    +
    + { item.name } +
    +
    +
    + +
    + + ))} + {!loading && user_types.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardUser_types; diff --git a/frontend/src/components/User_types/ListUser_types.tsx b/frontend/src/components/User_types/ListUser_types.tsx new file mode 100644 index 0000000..eb4f773 --- /dev/null +++ b/frontend/src/components/User_types/ListUser_types.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + user_types: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListUser_types = ({ user_types, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && user_types.map((item) => ( +
    + +
    + + +
    +

    IsAdmin

    +

    { dataFormatter.booleanFormatter(item.is_admin) }

    +
    + +
    +

    Name

    +

    { item.name }

    +
    + + + +
    +
    +
    + ))} + {!loading && user_types.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListUser_types diff --git a/frontend/src/components/User_types/TableUser_types.tsx b/frontend/src/components/User_types/TableUser_types.tsx new file mode 100644 index 0000000..2e37eb2 --- /dev/null +++ b/frontend/src/components/User_types/TableUser_types.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/user_types/user_typesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureUser_typesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleUser_types = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { user_types, loading, count, notify: user_typesNotify, refetch } = useAppSelector((state) => state.user_types) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (user_typesNotify.showNotification) { + notify(user_typesNotify.typeNotification, user_typesNotify.textNotification); + } + }, [user_typesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `user_types`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={user_types ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleUser_types diff --git a/frontend/src/components/User_types/configureUser_typesCols.tsx b/frontend/src/components/User_types/configureUser_typesCols.tsx new file mode 100644 index 0000000..d642d2d --- /dev/null +++ b/frontend/src/components/User_types/configureUser_typesCols.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'is_admin', + headerName: 'IsAdmin', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Users/CardUsers.tsx b/frontend/src/components/Users/CardUsers.tsx new file mode 100644 index 0000000..b98a2e4 --- /dev/null +++ b/frontend/src/components/Users/CardUsers.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + users: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardUsers = ({ + users, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && users.map((item, index) => ( +
    • + + {item.firstName} + + +
      + +
      +
    +
    + +
    +
    First Name
    +
    +
    + { item.firstName } +
    +
    +
    + +
    +
    Last Name
    +
    +
    + { item.lastName } +
    +
    +
    + +
    +
    Phone Number
    +
    +
    + { item.phoneNumber } +
    +
    +
    + +
    +
    E-Mail
    +
    +
    + { item.email } +
    +
    +
    + +
    +
    Disabled
    +
    +
    + { dataFormatter.booleanFormatter(item.disabled) } +
    +
    +
    + +
    +
    Avatar
    +
    +
    + { item.avatar } +
    +
    +
    + +
    + + ))} + {!loading && users.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardUsers; diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx new file mode 100644 index 0000000..05ac3f2 --- /dev/null +++ b/frontend/src/components/Users/ListUsers.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + users: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && users.map((item) => ( +
    + +
    + + +
    +

    First Name

    +

    { item.firstName }

    +
    + +
    +

    Last Name

    +

    { item.lastName }

    +
    + +
    +

    Phone Number

    +

    { item.phoneNumber }

    +
    + +
    +

    E-Mail

    +

    { item.email }

    +
    + +
    +

    Disabled

    +

    { dataFormatter.booleanFormatter(item.disabled) }

    +
    + +
    +

    Avatar

    +

    { item.avatar }

    +
    + + + +
    +
    +
    + ))} + {!loading && users.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListUsers diff --git a/frontend/src/components/Users/TableUsers.tsx b/frontend/src/components/Users/TableUsers.tsx new file mode 100644 index 0000000..0049bbf --- /dev/null +++ b/frontend/src/components/Users/TableUsers.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/users/usersSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureUsersCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { users, loading, count, notify: usersNotify, refetch } = useAppSelector((state) => state.users) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (usersNotify.showNotification) { + notify(usersNotify.typeNotification, usersNotify.textNotification); + } + }, [usersNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `users`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + delete data?.password; + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={users ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleUsers diff --git a/frontend/src/components/Users/configureUsersCols.tsx b/frontend/src/components/Users/configureUsersCols.tsx new file mode 100644 index 0000000..75554c8 --- /dev/null +++ b/frontend/src/components/Users/configureUsersCols.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'firstName', + headerName: 'First Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'lastName', + headerName: 'Last Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'phoneNumber', + headerName: 'Phone Number', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'email', + headerName: 'E-Mail', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'disabled', + headerName: 'Disabled', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'boolean', + + }, + + { + field: 'avatar', + headerName: 'Avatar', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Villages/CardVillages.tsx b/frontend/src/components/Villages/CardVillages.tsx new file mode 100644 index 0000000..ca96973 --- /dev/null +++ b/frontend/src/components/Villages/CardVillages.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + villages: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardVillages = ({ + villages, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
    + {loading && } +
      + {!loading && villages.map((item, index) => ( +
    • + + {item.name_en} + + +
      + +
      +
    +
    + +
    +
    NameKH
    +
    +
    + { item.name_kh } +
    +
    +
    + +
    +
    NameEN
    +
    +
    + { item.name_en } +
    +
    +
    + +
    +
    Commune
    +
    +
    + { dataFormatter.communesOneListFormatter(item.commune) } +
    +
    +
    + +
    + + ))} + {!loading && villages.length === 0 && ( +
    +

    No data to display

    +
    + )} + +
    + +
    + + ); +}; + +export default CardVillages; diff --git a/frontend/src/components/Villages/ListVillages.tsx b/frontend/src/components/Villages/ListVillages.tsx new file mode 100644 index 0000000..dba4f20 --- /dev/null +++ b/frontend/src/components/Villages/ListVillages.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + villages: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListVillages = ({ villages, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && villages.map((item) => ( +
    + +
    + + +
    +

    NameKH

    +

    { item.name_kh }

    +
    + +
    +

    NameEN

    +

    { item.name_en }

    +
    + +
    +

    Commune

    +

    { dataFormatter.communesOneListFormatter(item.commune) }

    +
    + + + +
    +
    +
    + ))} + {!loading && villages.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ) +}; + +export default ListVillages diff --git a/frontend/src/components/Villages/TableVillages.tsx b/frontend/src/components/Villages/TableVillages.tsx new file mode 100644 index 0000000..16d7219 --- /dev/null +++ b/frontend/src/components/Villages/TableVillages.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/villages/villagesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureVillagesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleVillages = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { villages, loading, count, notify: villagesNotify, refetch } = useAppSelector((state) => state.villages) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (villagesNotify.showNotification) { + notify(villagesNotify.typeNotification, villagesNotify.textNotification); + } + }, [villagesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `villages`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={villages ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
    + <> + {filterItems && filterItems.map((filterItem) => { + return ( +
    +
    +
    Filter
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
    +
    + Value +
    + + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
    + ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
    +
    +
    From
    + +
    +
    +
    To
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    To
    + +
    +
    + ) : ( +
    +
    Contains
    + +
    + )} +
    +
    Action
    + { + deleteFilter(filterItem.id) + }} + /> +
    +
    + ) + })} +
    + + +
    + +
    +
    +
    : null + } + +

    Are you sure you want to delete this item?

    +
    + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleVillages diff --git a/frontend/src/components/Villages/configureVillagesCols.tsx b/frontend/src/components/Villages/configureVillagesCols.tsx new file mode 100644 index 0000000..7ef5453 --- /dev/null +++ b/frontend/src/components/Villages/configureVillagesCols.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name_kh', + headerName: 'NameKH', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'name_en', + headerName: 'NameEN', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'commune', + headerName: 'Commune', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('communes'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
    + +
    , + ] + }, + }, + ]; +}; diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..a9783c8 --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,15 @@ +export const hostApi = process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_BACK_API ? 'http://localhost' : '' +export const portApi = process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_BACK_API ? 8080 : ''; +export const baseURLApi = `${hostApi}${portApi ? `:${portApi}` : ``}/api` + +export const localStorageDarkModeKey = 'darkMode' + +export const localStorageStyleKey = 'style' + +export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20' + +export const appTitle = 'created by Flatlogic generator!' + +export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}` + +export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || '' diff --git a/frontend/src/css/_app.css b/frontend/src/css/_app.css new file mode 100644 index 0000000..affc655 --- /dev/null +++ b/frontend/src/css/_app.css @@ -0,0 +1,32 @@ +html { + @apply h-full; +} + +body { + @apply pt-14 xl:pl-60 h-full; +} + +#app { + @apply w-screen transition-position lg:w-auto h-full flex flex-col; +} + +.dropdown { + @apply cursor-pointer; +} + +li.stack-item:not(:last-child):after { + content: '/'; + @apply inline-block pl-2; +} + +.m-clipped, .m-clipped body { + @apply overflow-hidden lg:overflow-visible; +} + +.full-screen body { + @apply p-0; +} + +.main-navbar, .app-sidebar-brand { + box-shadow: 0px -1px 40px rgba(112, 144, 176, 0.2); +} diff --git a/frontend/src/css/_calendar.css b/frontend/src/css/_calendar.css new file mode 100644 index 0000000..ce44e3f --- /dev/null +++ b/frontend/src/css/_calendar.css @@ -0,0 +1,37 @@ +.rbc-event { + @apply bg-blue-600 !important; +} + +.rbc-show-more { + @apply dark:text-white bg-transparent !important; +} + +.rbc-btn-group button { + @apply dark:text-white !important; +} + +.rbc-btn-group button:hover { + @apply text-white dark:bg-dark-700 !important; +} + +.rbc-btn-group button.rbc-active { + @apply text-black dark:bg-blue-600 !important; +} + +.rbc-btn-group button:focus { + @apply dark:bg-blue-600 !important; +} + +.rbc-day-bg.rbc-off-range-bg { + @apply dark:bg-dark-800 !important; +} +.rbc-current-time-indicator{ + @apply h-1 !important; +} +.rbc-today { + @apply dark:bg-dark-600/40 !important; +} + +.rbc-day-bg.rbc-selected-cell { + @apply dark:bg-dark-500 !important; +} diff --git a/frontend/src/css/_checkbox-radio-switch.css b/frontend/src/css/_checkbox-radio-switch.css new file mode 100644 index 0000000..a7f567b --- /dev/null +++ b/frontend/src/css/_checkbox-radio-switch.css @@ -0,0 +1,59 @@ +@layer components { + .checkbox, .radio, .switch { + @apply inline-flex items-center cursor-pointer relative; + } + + .checkbox input[type=checkbox], .radio input[type=radio], .switch input[type=checkbox] { + @apply absolute left-0 opacity-0 -z-1; + } + + .checkbox input[type=checkbox]+.check, .radio input[type=radio]+.check, .switch input[type=checkbox]+.check { + @apply border-gray-700 border transition-colors duration-200 dark:bg-slate-800; + } + + .checkbox input[type=checkbox]:focus+.check, .radio input[type=radio]:focus+.check, .switch input[type=checkbox]:focus+.check { + @apply ring ring-blue-700; + } + + .checkbox input[type=checkbox]+.check, .radio input[type=radio]+.check { + @apply block w-5 h-5; + } + + .checkbox input[type=checkbox]+.check { + @apply rounded; + } + + .switch input[type=checkbox]+.check { + @apply flex items-center shrink-0 w-12 h-6 p-0.5 bg-gray-200; + } + + .radio input[type=radio]+.check, .switch input[type=checkbox]+.check, .switch input[type=checkbox]+.check:before { + @apply rounded-full; + } + + .checkbox input[type=checkbox]:checked+.check, .radio input[type=radio]:checked+.check { + @apply bg-no-repeat bg-center border-4; + } + + .checkbox input[type=checkbox]:checked+.check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E"); + } + + .radio input[type=radio]:checked+.check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E"); + } + + .switch input[type=checkbox]:checked+.check, .checkbox input[type=checkbox]:checked+.check, .radio input[type=radio]:checked+.check { + @apply bg-blue-600 border-blue-600; + } + + .switch input[type=checkbox]+.check:before { + content: ''; + @apply block w-5 h-5 bg-white border border-gray-700; + } + + .switch input[type=checkbox]:checked+.check:before { + transform: translate3d(110%, 0 ,0); + @apply border-blue-600; + } +} \ No newline at end of file diff --git a/frontend/src/css/_helper.css b/frontend/src/css/_helper.css new file mode 100644 index 0000000..3444fa9 --- /dev/null +++ b/frontend/src/css/_helper.css @@ -0,0 +1,23 @@ +.helper-container { + right: 0; + top: 70px; + transform: translateX(100%); + + .tab { + top: 0; + left: 0; + transform: translateX(-100%); + } + + .tab:hover { + @apply bg-gray-900 cursor-pointer; + } +} + +.helper-container.open { + transform: translateX(0); +} + +.react-datepicker-wrapper, .react-datepicker-popper { + z-index: 10 !important; +} diff --git a/frontend/src/css/_progress.css b/frontend/src/css/_progress.css new file mode 100644 index 0000000..65d6796 --- /dev/null +++ b/frontend/src/css/_progress.css @@ -0,0 +1,21 @@ +@layer base { + progress { + @apply h-3 rounded-full overflow-hidden; + } + + progress::-webkit-progress-bar { + @apply bg-blue-200; + } + + progress::-webkit-progress-value { + @apply bg-blue-500; + } + + progress::-moz-progress-bar { + @apply bg-blue-500; + } + + progress::-ms-fill { + @apply bg-blue-500 border-0; + } +} diff --git a/frontend/src/css/_scrollbars.css b/frontend/src/css/_scrollbars.css new file mode 100644 index 0000000..a181b84 --- /dev/null +++ b/frontend/src/css/_scrollbars.css @@ -0,0 +1,41 @@ +@layer base { + html { + scrollbar-width: thin; + scrollbar-color: rgb(156, 163, 175) rgb(249, 250, 251); + } + + body::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + body::-webkit-scrollbar-track { + @apply bg-gray-50; + } + + body::-webkit-scrollbar-thumb { + @apply bg-gray-400 rounded; + } + + body::-webkit-scrollbar-thumb:hover { + @apply bg-gray-500; + } +} + +@layer utilities { + .dark-scrollbars-compat { + scrollbar-color: rgb(71, 85, 105) rgb(30, 41, 59); + } + + .dark-scrollbars::-webkit-scrollbar-track { + @apply bg-slate-800; + } + + .dark-scrollbars::-webkit-scrollbar-thumb { + @apply bg-slate-600; + } + + .dark-scrollbars::-webkit-scrollbar-thumb:hover { + @apply bg-slate-500; + } +} diff --git a/frontend/src/css/_select-dropdown.css b/frontend/src/css/_select-dropdown.css new file mode 100644 index 0000000..040ae0d --- /dev/null +++ b/frontend/src/css/_select-dropdown.css @@ -0,0 +1,31 @@ +.react-select__control { + @apply dark:bg-dark-800 dark:border-dark-700 !important; +} + +.react-select__single-value { + @apply dark:text-white !important; +} + +.react-select__menu { + @apply dark:border-dark-700 +} + +.react-select__menu-list { + @apply dark:bg-dark-800 dark:border-dark-700 dark:rounded !important; +} + +.react-select__option { + @apply cursor-pointer hover:bg-gray-200 dark:hover:bg-dark-700 !important; +} + +.react-select__option--is-focused { + @apply dark:bg-dark-800 dark:text-white hover:dark:bg-dark-700 hover:dark:text-white !important; +} + +.react-select__option--is-selected, .react-select__option--is-selected:hover { + @apply dark:bg-dark-600 !important; +} + +.react-select__multi-value__remove { + @apply dark:bg-dark-600 dark:text-white hover:dark:bg-red-300 hover:dark:text-red-600 !important; +} diff --git a/frontend/src/css/_table.css b/frontend/src/css/_table.css new file mode 100644 index 0000000..fc149c9 --- /dev/null +++ b/frontend/src/css/_table.css @@ -0,0 +1,115 @@ +@layer base { + table { + @apply w-full; + } + + thead { + @apply hidden lg:table-header-group; + } + + tr { + @apply max-w-full block relative border-b-4 border-gray-100 + lg:table-row lg:border-b-0 dark:border-slate-800; + } + + tr:last-child { + @apply border-b-0; + } + + td:not(:first-child) { + @apply lg:border-l lg:border-t-0 lg:border-r-0 lg:border-b-0 lg:border-gray-100 lg:dark:border-slate-700; + } + + th { + @apply lg:text-left lg:p-3 border-b; + } + + th.sortable { + cursor: pointer; + } + + th.sortable:hover:after { + transition: all 1s; + position: absolute; + + content: "↕"; + + margin-left: 1rem; + } + + th.sortable.asc:hover:after { + content: "↑"; + } + th.sortable.desc:hover:after { + content: "↓"; + } + + td { + @apply flex justify-between text-right py-3 px-4 align-top border-b border-gray-100 + lg:table-cell lg:text-left lg:p-3 lg:align-middle lg:border-b-0 dark:border-slate-800 dark:text-white; + } + + td:last-child { + @apply border-b-0; + } + + tbody tr, tbody tr:nth-child(odd) { + @apply lg:hover:bg-brand-300/70; + } + + tbody tr:nth-child(even) { + @apply lg:bg-brand-300 dark:bg-brand-300/70; + } + + td:before { + content: attr(data-label); + @apply font-semibold pr-3 text-left lg:hidden; + } + + tbody tr td { + @apply text-sm font-normal text-brand-900 dark:text-white; + } + + .datagrid--table, .MuiDataGrid-root { + @apply rounded border-none !important; + } + + .datagrid--header { + @apply uppercase !important; + } + + .datagrid--header, + .datagrid--header .MuiIconButton-root, + .datagrid--cell, + .datagrid--cell .MuiIconButton-root { + @apply dark:text-white; + } + + .datagrid--cell .MuiDataGrid-booleanCell { + @apply dark:text-white !important; + } + + .datagrid--cell .MuiIconButton-root:hover { + @apply dark:text-white dark:bg-dark-700; + } + + .datagrid--row { + @apply even:bg-gray-100 dark:even:bg-[#1B1D22] dark:odd:bg-dark-900 !important; + } + + .datagrid--table .MuiTablePagination-root { + @apply dark:text-white; + } + + .datagrid--table .MuiTablePagination-root .MuiButtonBase-root:disabled { + @apply dark:text-dark-700; + } + + .datagrid--table .MuiTablePagination-root .MuiButtonBase-root:hover { + @apply dark:bg-dark-700; + } + + .MuiButton-colorInherit { + @apply text-blue-600 dark:text-dark-700 !important; + } +} diff --git a/frontend/src/css/_theme.css b/frontend/src/css/_theme.css new file mode 100644 index 0000000..489bdf5 --- /dev/null +++ b/frontend/src/css/_theme.css @@ -0,0 +1,103 @@ + +.theme-pink { +.app-sidebar { + @apply bg-brand-900 text-white; + +.menu-title, +.menu-item-icon, +.menu-item-link { + @apply text-white; +} +} + +.app-sidebar-brand { + @apply bg-white; +} + +.bg-blue-600 { + @apply bg-brand-800; +} + +.border-blue-700 { + @apply border-pink-700; +} + +.checkbox input[type=checkbox]:checked + .check, .radio input[type=radio]:checked + .check { + @apply border-brand-800; +} + +.helper-container .tab { + @apply bg-brand-900; +} + +.focus\:ring:focus { + --tw-ring-color: #14142A; +} + +.checkbox input[type=checkbox]:focus + .check, .radio input[type=radio]:focus + .check, .switch input[type=checkbox]:focus + .check { + --tw-ring-color: #14142A; +} + +} + +.theme-green { + +.app-sidebar { + @apply bg-brand-800 text-white; + +.menu-title, +.menu-item-icon, +.menu-item-link { + @apply text-white; +} + +} + +.app-sidebar-brand { + @apply bg-white; +} + +.bg-blue-600 { + @apply bg-brand-800; +} + +.border-blue-700 { + @apply bg-brand-700; +} + +.hover\:bg-blue-700:hover { + @apply bg-brand-700; +} + +.text-blue-600 { + @apply text-brand-900; +} + +.checkbox input[type=checkbox]:checked + .check, .radio input[type=radio]:checked + .check { + @apply border-brand-800; +} + +.helper-container .tab { + @apply bg-brand-700; +} + +.focus\:ring:focus { + --tw-ring-color: #4E4B66; +} + +.checkbox input[type=checkbox]:focus + .check, .radio input[type=radio]:focus + .check, .switch input[type=checkbox]:focus + .check { + --tw-ring-color: #4E4B66; +} + +.text-blue-500 { + @apply text-brand-800; +} + +.hover\:text-blue-600:hover { + @apply text-brand-800; +} + +.active\:text-blue-700:active { + @apply text-brand-800; +} +} diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css new file mode 100644 index 0000000..2886dd7 --- /dev/null +++ b/frontend/src/css/main.css @@ -0,0 +1,33 @@ +@import "tailwind/_base.css"; +@import "tailwind/_components.css"; +@import "tailwind/_utilities.css"; +@import 'intro.js/introjs.css'; +@import "_checkbox-radio-switch.css"; +@import "_progress.css"; +@import "_scrollbars.css"; +@import "_table.css"; +@import "_helper.css"; +@import '_calendar.css'; +@import '_select-dropdown.css'; +@import "_theme.css"; + +.introjs-tooltip { + @apply min-w-[400px] max-w-[480px] p-2 !important; +} + +.good-img { + @apply -mt-96 !important; +} +.end-img { + @apply -mt-72 !important; +} +.introjs-button { + @apply bg-blue-600 text-white !important; + text-shadow: none !important; +} +.introjs-bullets ul li a.active { + @apply bg-blue-600 !important; +} +.introjs-prevbutton{ + @apply bg-transparent border border-blue-600 text-blue-600 !important; +} diff --git a/frontend/src/css/tailwind/_base.css b/frontend/src/css/tailwind/_base.css new file mode 100644 index 0000000..2f02db5 --- /dev/null +++ b/frontend/src/css/tailwind/_base.css @@ -0,0 +1 @@ +@tailwind base; diff --git a/frontend/src/css/tailwind/_components.css b/frontend/src/css/tailwind/_components.css new file mode 100644 index 0000000..020aaba --- /dev/null +++ b/frontend/src/css/tailwind/_components.css @@ -0,0 +1 @@ +@tailwind components; diff --git a/frontend/src/css/tailwind/_utilities.css b/frontend/src/css/tailwind/_utilities.css new file mode 100644 index 0000000..65dd5f6 --- /dev/null +++ b/frontend/src/css/tailwind/_utilities.css @@ -0,0 +1 @@ +@tailwind utilities; diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js new file mode 100644 index 0000000..0554925 --- /dev/null +++ b/frontend/src/helpers/dataFormatter.js @@ -0,0 +1,555 @@ +import dayjs from 'dayjs'; +import _ from 'lodash'; + +export default { + filesFormatter(arr) { + if (!arr || !arr.length) return []; + return arr.map((item) => item); + }, + imageFormatter(arr) { + if (!arr || !arr.length) return [] + return arr.map(item => ({ + publicUrl: item.publicUrl || '' + })) + }, + oneImageFormatter(arr) { + if (!arr || !arr.length) return '' + return arr[0].publicUrl || '' + }, + dateFormatter(date) { + if (!date) return '' + return dayjs(date).format('YYYY-MM-DD') + }, + dateTimeFormatter(date) { + if (!date) return '' + return dayjs(date).format('YYYY-MM-DD HH:mm') + }, + booleanFormatter(val) { + return val ? 'Yes' : 'No' + }, + dataGridEditFormatter(obj) { + return _.transform(obj, (result, value, key) => { + if (_.isArray(value)) { + result[key] = _.map(value, 'id'); + } else if (_.isObject(value)) { + result[key] = value.id; + } else { + result[key] = value; + } + }); + }, + + usersManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.firstName) + }, + usersOneListFormatter(val) { + if (!val) return '' + return val.firstName + }, + usersManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.firstName} + }); + }, + usersOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.firstName, id: val.id} + }, + + branchesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + branchesOneListFormatter(val) { + if (!val) return '' + return val.name + }, + branchesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + branchesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + client_statusManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + client_statusOneListFormatter(val) { + if (!val) return '' + return val.name + }, + client_statusManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + client_statusOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + clientsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + clientsOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + clientsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + clientsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + communesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + communesOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + communesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + communesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + depositsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.deposit_amount) + }, + depositsOneListFormatter(val) { + if (!val) return '' + return val.deposit_amount + }, + depositsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.deposit_amount} + }); + }, + depositsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.deposit_amount, id: val.id} + }, + + districtsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + districtsOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + districtsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + districtsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + document_typeManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + document_typeOneListFormatter(val) { + if (!val) return '' + return val.name + }, + document_typeManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + document_typeOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + expense_itemsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.description) + }, + expense_itemsOneListFormatter(val) { + if (!val) return '' + return val.description + }, + expense_itemsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.description} + }); + }, + expense_itemsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.description, id: val.id} + }, + + expense_typesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + expense_typesOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + expense_typesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + expense_typesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + group_menusManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + group_menusOneListFormatter(val) { + if (!val) return '' + return val.name + }, + group_menusManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + group_menusOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + interest_ratesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + interest_ratesOneListFormatter(val) { + if (!val) return '' + return val.name + }, + interest_ratesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + interest_ratesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + loan_statusManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + loan_statusOneListFormatter(val) { + if (!val) return '' + return val.name + }, + loan_statusManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + loan_statusOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + loan_typesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + loan_typesOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + loan_typesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + loan_typesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + loansManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.code) + }, + loansOneListFormatter(val) { + if (!val) return '' + return val.code + }, + loansManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.code} + }); + }, + loansOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.code, id: val.id} + }, + + menusManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.label) + }, + menusOneListFormatter(val) { + if (!val) return '' + return val.label + }, + menusManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.label} + }); + }, + menusOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.label, id: val.id} + }, + + payment_revenuesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.transaction_date) + }, + payment_revenuesOneListFormatter(val) { + if (!val) return '' + return val.transaction_date + }, + payment_revenuesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.transaction_date} + }); + }, + payment_revenuesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.transaction_date, id: val.id} + }, + + payment_statusManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + payment_statusOneListFormatter(val) { + if (!val) return '' + return val.name + }, + payment_statusManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + payment_statusOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + paymentsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.remark) + }, + paymentsOneListFormatter(val) { + if (!val) return '' + return val.remark + }, + paymentsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.remark} + }); + }, + paymentsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.remark, id: val.id} + }, + + provincesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + provincesOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + provincesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + provincesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + sexesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + sexesOneListFormatter(val) { + if (!val) return '' + return val.name + }, + sexesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + sexesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + shareholdersManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + shareholdersOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + shareholdersManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + shareholdersOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + staff_statusManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + staff_statusOneListFormatter(val) { + if (!val) return '' + return val.name + }, + staff_statusManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + staff_statusOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + staffsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + staffsOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + staffsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + staffsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + + urlsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.uri) + }, + urlsOneListFormatter(val) { + if (!val) return '' + return val.uri + }, + urlsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.uri} + }); + }, + urlsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.uri, id: val.id} + }, + + user_typesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + user_typesOneListFormatter(val) { + if (!val) return '' + return val.name + }, + user_typesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + user_typesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + + villagesManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name_en) + }, + villagesOneListFormatter(val) { + if (!val) return '' + return val.name_en + }, + villagesManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name_en} + }); + }, + villagesOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name_en, id: val.id} + }, + +} diff --git a/frontend/src/helpers/fileSaver.ts b/frontend/src/helpers/fileSaver.ts new file mode 100644 index 0000000..eff647d --- /dev/null +++ b/frontend/src/helpers/fileSaver.ts @@ -0,0 +1,6 @@ +import { saveAs } from "file-saver"; + +export const saveFile = (e, url: string, name: string) => { + e.stopPropagation(); + saveAs(url,name); +}; diff --git a/frontend/src/helpers/humanize.ts b/frontend/src/helpers/humanize.ts new file mode 100644 index 0000000..ea88c06 --- /dev/null +++ b/frontend/src/helpers/humanize.ts @@ -0,0 +1,11 @@ +export function humanize(str: string) { + if (!str) { + return ''; + } + return str.toString() + .replace(/^[\s_]+|[\s_]+$/g, '') + .replace(/[_\s]+/g, ' ') + .replace(/^[a-z]/, function (m) { + return m.toUpperCase(); + }); + } \ No newline at end of file diff --git a/frontend/src/helpers/notifyStateHandler.ts b/frontend/src/helpers/notifyStateHandler.ts new file mode 100644 index 0000000..b1a247c --- /dev/null +++ b/frontend/src/helpers/notifyStateHandler.ts @@ -0,0 +1,31 @@ +export const resetNotify = (state) => { + state.notify.showNotification = false; + state.notify.typeNotification = ''; + state.notify.textNotification = ''; +}; +export const rejectNotify = (state, action) => { + if (typeof action.payload === 'string') { + state.notify.textNotification = action.payload; + } else if (typeof action === 'object') { + const obj = { ...action.payload?.errors }; + delete obj['_errors']; + + let msg = ''; + + for (const key in obj) { + msg += `${key}: ${obj[key]['_errors']}; \n `; + } + + state.notify.textNotification = msg; + } else { + state.notify.textNotification = 'Network error'; + } + state.notify.textNotification = state.notify.textNotification || 'Network error'; + state.notify.typeNotification = 'error'; + state.notify.showNotification = true; +}; +export const fulfilledNotify = (state, msg) => { + state.notify.textNotification = msg; + state.notify.typeNotification = 'success'; + state.notify.showNotification = true; +}; diff --git a/frontend/src/hooks/sampleData.ts b/frontend/src/hooks/sampleData.ts new file mode 100644 index 0000000..7e2aeec --- /dev/null +++ b/frontend/src/hooks/sampleData.ts @@ -0,0 +1,22 @@ +import useSWR from 'swr' +const fetcher = (url: string) => fetch(url).then((res) => res.json()) + +export const useSampleClients = () => { + const { data, error } = useSWR('/data-sources/clients.json', fetcher) + + return { + clients: data?.data ?? [], + isLoading: !error && !data, + isError: error, + } +} + +export const useSampleTransactions = () => { + const { data, error } = useSWR('/data-sources/history.json', fetcher) + + return { + transactions: data?.data ?? [], + isLoading: !error && !data, + isError: error, + } +} diff --git a/frontend/src/hooks/useDevCompilationStatus.ts b/frontend/src/hooks/useDevCompilationStatus.ts new file mode 100644 index 0000000..dd07dba --- /dev/null +++ b/frontend/src/hooks/useDevCompilationStatus.ts @@ -0,0 +1,44 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +type CompilationStatus = 'ready' | 'compiling' | 'error' | 'initial'; + +const useDevCompilationStatus = (): CompilationStatus => { + const router = useRouter(); + const [status, setStatus] = useState('initial'); + + useEffect(() => { + if (process.env.NODE_ENV !== 'development') { + setStatus('ready'); + return; + } + + const handleRouteChangeStart = () => { + setStatus('compiling'); + }; + + const handleRouteChangeComplete = () => { + setTimeout(() => setStatus('ready'), 300); + }; + + const handleRouteChangeError = () => { + setTimeout(() => setStatus('error'), 300); + }; + + router.events.on('routeChangeStart', handleRouteChangeStart); + router.events.on('routeChangeComplete', handleRouteChangeComplete); + router.events.on('routeChangeError', handleRouteChangeError); + + setStatus('ready'); + + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart); + router.events.off('routeChangeComplete', handleRouteChangeComplete); + router.events.off('routeChangeError', handleRouteChangeError); + }; + }, [router]); + + return status; +}; + +export default useDevCompilationStatus; \ No newline at end of file diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts new file mode 100644 index 0000000..0c7dd74 --- /dev/null +++ b/frontend/src/interfaces/index.ts @@ -0,0 +1,109 @@ +export type UserPayloadObject = { + name: string + email: string + avatar: string +} + +export type MenuAsideItem = { + label: string + icon?: string + href?: string + target?: string + color?: ColorButtonKey + isLogout?: boolean + withDevider?: boolean; + menu?: MenuAsideItem[] + permissions?: string | string[] +} + +export type MenuNavBarItem = { + label?: string + icon?: string + href?: string + target?: string + isDivider?: boolean + isLogout?: boolean + isDesktopNoLabel?: boolean + isToggleLightDark?: boolean + isCurrentUser?: boolean + menu?: MenuNavBarItem[] +} + +export type ColorKey = 'white' | 'light' | 'contrast' | 'success' | 'danger' | 'warning' | 'info' + +export type ColorButtonKey = + | 'white' + | 'whiteDark' + | 'lightDark' + | 'contrast' + | 'success' + | 'danger' + | 'warning' + | 'info' + | 'void' + +export type BgKey = 'purplePink' | 'pinkRed' | 'violet' + +export type TrendType = 'up' | 'down' | 'success' | 'danger' | 'warning' | 'info' + +export type TransactionType = 'withdraw' | 'deposit' | 'invoice' | 'payment' + +export type Transaction = { + id: number + amount: number + account: string + name: string + date: string + type: TransactionType + business: string +} + +export type Client = { + id: number + avatar: string + login: string + name: string + city: string, + company: string + firstName: string + lastName: string + phoneNumber: string + email: string + progress: number, + role: string + disabled: boolean + created: string + created_mm_dd_yyyy: string +} + +export interface User { + id: string; + firstName: string; + lastName?: any; + phoneNumber?: any; + email: string; + role: string; + disabled: boolean; + password: string; + emailVerified: boolean; + emailVerificationToken?: any; + emailVerificationTokenExpiresAt?: any; + passwordResetToken?: any; + passwordResetTokenExpiresAt?: any; + provider: string; + importHash?: any; + createdAt: Date; + updatedAt: Date; + deletedAt?: any; + createdById?: any; + updatedById?: any; + avatar: any[]; + notes: any[]; +} + +export type StyleKey = 'white' | 'basic' + +export type UserForm = { + name: string + email: string +} diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx new file mode 100644 index 0000000..36ccc7e --- /dev/null +++ b/frontend/src/layouts/Authenticated.tsx @@ -0,0 +1,111 @@ +import React, { ReactNode, useEffect } from 'react' +import { useState } from 'react' +import jwt from 'jsonwebtoken'; +import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' +import menuAside from '../menuAside' +import menuNavBar from '../menuNavBar' +import BaseIcon from '../components/BaseIcon' +import NavBar from '../components/NavBar' +import NavBarItemPlain from '../components/NavBarItemPlain' +import AsideMenu from '../components/AsideMenu' +import FooterBar from '../components/FooterBar' +import { useAppDispatch, useAppSelector } from '../stores/hooks' +import Search from '../components/Search'; +import { useRouter } from 'next/router' +import {findMe, logoutUser} from "../stores/authSlice"; + +type Props = { + children: ReactNode +} + +export default function LayoutAuthenticated({ + children, +}: Props) { + const dispatch = useAppDispatch() + const router = useRouter() + const { token, currentUser } = useAppSelector((state) => state.auth) + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + let localToken + if (typeof window !== 'undefined') { + // Perform localStorage action + localToken = localStorage.getItem('token') + } + + const isTokenValid = () => { + const token = localStorage.getItem('token'); + if (!token) return; + const date = new Date().getTime() / 1000; + const data = jwt.decode(token); + if (!data) return; + return date < data.exp; + }; + + useEffect(() => { + dispatch(findMe()); + if (!isTokenValid()) { + dispatch(logoutUser()); + router.push('/login'); + } + }, [token, localToken]); + + const darkMode = useAppSelector((state) => state.style.darkMode) + + const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false) + const [isAsideLgActive, setIsAsideLgActive] = useState(false) + + useEffect(() => { + const handleRouteChangeStart = () => { + setIsAsideMobileExpanded(false) + setIsAsideLgActive(false) + } + + router.events.on('routeChangeStart', handleRouteChangeStart) + + // If the component is unmounted, unsubscribe + // from the event with the `off` method: + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart) + } + }, [router.events, dispatch]) + + const layoutAsidePadding = 'xl:pl-60' + + return ( +
    +
    + + setIsAsideMobileExpanded(!isAsideMobileExpanded)} + > + + + setIsAsideLgActive(true)} + > + + + + + + + setIsAsideLgActive(false)} + /> + {children} + Hand-crafted & Made with ❤️ +
    +
    + ) +} diff --git a/frontend/src/layouts/Guest.tsx b/frontend/src/layouts/Guest.tsx new file mode 100644 index 0000000..657813c --- /dev/null +++ b/frontend/src/layouts/Guest.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react' +import { useAppSelector } from '../stores/hooks' + +type Props = { + children: ReactNode +} + +export default function LayoutGuest({ children }: Props) { + const darkMode = useAppSelector((state) => state.style.darkMode) + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + + return ( +
    +
    {children}
    +
    + ) +} diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts new file mode 100644 index 0000000..ae8f51b --- /dev/null +++ b/frontend/src/menuAside.ts @@ -0,0 +1,290 @@ +import * as icon from '@mdi/js'; +import { MenuAsideItem } from './interfaces' + +const menuAside: MenuAsideItem[] = [ + { + href: '/dashboard', + icon: icon.mdiViewDashboardOutline, + label: 'Dashboard', + }, + + { + href: '/users/users-list', + label: 'Users', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiAccountGroup ?? icon.mdiTable, + permissions: 'READ_USERS' + }, + { + href: '/branches/branches-list', + label: 'Branches', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiOfficeBuilding' in icon ? icon['mdiOfficeBuilding' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_BRANCHES' + }, + { + href: '/calendars/calendars-list', + label: 'Calendars', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCalendar' in icon ? icon['mdiCalendar' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_CALENDARS' + }, + { + href: '/client_status/client_status-list', + label: 'Client status', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountCheck' in icon ? icon['mdiAccountCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_CLIENT_STATUS' + }, + { + href: '/clients/clients-list', + label: 'Clients', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_CLIENTS' + }, + { + href: '/communes/communes-list', + label: 'Communes', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_COMMUNES' + }, + { + href: '/deposits/deposits-list', + label: 'Deposits', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiBank' in icon ? icon['mdiBank' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_DEPOSITS' + }, + { + href: '/districts/districts-list', + label: 'Districts', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiMapMarkerRadius' in icon ? icon['mdiMapMarkerRadius' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_DISTRICTS' + }, + { + href: '/document_type/document_type-list', + label: 'Document type', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiFileDocument' in icon ? icon['mdiFileDocument' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_DOCUMENT_TYPE' + }, + { + href: '/expense_items/expense_items-list', + label: 'Expense items', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCurrencyUsd' in icon ? icon['mdiCurrencyUsd' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_EXPENSE_ITEMS' + }, + { + href: '/expense_types/expense_types-list', + label: 'Expense types', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiTagTextOutline' in icon ? icon['mdiTagTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_EXPENSE_TYPES' + }, + { + href: '/group_menus/group_menus-list', + label: 'Group menus', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiMenu' in icon ? icon['mdiMenu' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_GROUP_MENUS' + }, + { + href: '/guarantor/guarantor-list', + label: 'Guarantor', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountTie' in icon ? icon['mdiAccountTie' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_GUARANTOR' + }, + { + href: '/interest_rates/interest_rates-list', + label: 'Interest rates', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiPercent' in icon ? icon['mdiPercent' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_INTEREST_RATES' + }, + { + href: '/loan_status/loan_status-list', + label: 'Loan status', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCheckCircleOutline' in icon ? icon['mdiCheckCircleOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_LOAN_STATUS' + }, + { + href: '/loan_types/loan_types-list', + label: 'Loan types', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_LOAN_TYPES' + }, + { + href: '/loans/loans-list', + label: 'Loans', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_LOANS' + }, + { + href: '/members/members-list', + label: 'Members', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountMultiple' in icon ? icon['mdiAccountMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_MEMBERS' + }, + { + href: '/menus/menus-list', + label: 'Menus', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiMenuOpen' in icon ? icon['mdiMenuOpen' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_MENUS' + }, + { + href: '/payment_revenues/payment_revenues-list', + label: 'Payment revenues', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCurrencyUsdCircle' in icon ? icon['mdiCurrencyUsdCircle' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PAYMENT_REVENUES' + }, + { + href: '/payment_status/payment_status-list', + label: 'Payment status', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCheckCircle' in icon ? icon['mdiCheckCircle' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PAYMENT_STATUS' + }, + { + href: '/payment_transactions/payment_transactions-list', + label: 'Payment transactions', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PAYMENT_TRANSACTIONS' + }, + { + href: '/payments/payments-list', + label: 'Payments', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCreditCard' in icon ? icon['mdiCreditCard' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PAYMENTS' + }, + { + href: '/provinces/provinces-list', + label: 'Provinces', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiMapOutline' in icon ? icon['mdiMapOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PROVINCES' + }, + { + href: '/sexes/sexes-list', + label: 'Sexes', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiGenderMaleFemale' in icon ? icon['mdiGenderMaleFemale' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_SEXES' + }, + { + href: '/shareholders/shareholders-list', + label: 'Shareholders', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountStar' in icon ? icon['mdiAccountStar' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_SHAREHOLDERS' + }, + { + href: '/staff_status/staff_status-list', + label: 'Staff status', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiCheckCircleOutline' in icon ? icon['mdiCheckCircleOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_STAFF_STATUS' + }, + { + href: '/staffs/staffs-list', + label: 'Staffs', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountTie' in icon ? icon['mdiAccountTie' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_STAFFS' + }, + { + href: '/urls/urls-list', + label: 'Urls', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_URLS' + }, + { + href: '/user_has_menu/user_has_menu-list', + label: 'User has menu', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountCog' in icon ? icon['mdiAccountCog' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_USER_HAS_MENU' + }, + { + href: '/user_type_urls/user_type_urls-list', + label: 'User type urls', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiLinkVariantPlus' in icon ? icon['mdiLinkVariantPlus' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_USER_TYPE_URLS' + }, + { + href: '/user_types/user_types-list', + label: 'User types', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiAccountGroupOutline' in icon ? icon['mdiAccountGroupOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_USER_TYPES' + }, + { + href: '/villages/villages-list', + label: 'Villages', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiMapMarkerOutline' in icon ? icon['mdiMapMarkerOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_VILLAGES' + }, + { + href: '/profile', + label: 'Profile', + icon: icon.mdiAccountCircle, + }, + + { + href: '/api-docs', + target: '_blank', + label: 'Swagger API', + icon: icon.mdiFileCode, + permissions: 'READ_API_DOCS' + }, +] + +export default menuAside diff --git a/frontend/src/menuNavBar.ts b/frontend/src/menuNavBar.ts new file mode 100644 index 0000000..8f972ea --- /dev/null +++ b/frontend/src/menuNavBar.ts @@ -0,0 +1,49 @@ +import { + mdiMenu, + mdiClockOutline, + mdiCloud, + mdiCrop, + mdiAccount, + mdiCogOutline, + mdiEmail, + mdiLogout, + mdiThemeLightDark, + mdiGithub, + mdiVuejs, +} from '@mdi/js' +import { MenuNavBarItem } from './interfaces' + +const menuNavBar: MenuNavBarItem[] = [ + { + isCurrentUser: true, + menu: [ + { + icon: mdiAccount, + label: 'My Profile', + href: '/profile', + }, + { + isDivider: true, + }, + { + icon: mdiLogout, + label: 'Log Out', + isLogout: true, + }, + ], + }, + { + icon: mdiThemeLightDark, + label: 'Light/Dark', + isDesktopNoLabel: true, + isToggleLightDark: true, + }, + { + icon: mdiLogout, + label: 'Log out', + isDesktopNoLabel: true, + isLogout: true, + }, +] + +export default menuNavBar diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx new file mode 100644 index 0000000..9a3f242 --- /dev/null +++ b/frontend/src/pages/_app.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import type { AppProps } from 'next/app'; +import type { ReactElement, ReactNode } from 'react'; +import type { NextPage } from 'next'; +import Head from 'next/head'; +import { store } from '../stores/store'; +import { Provider } from 'react-redux'; +import '../css/main.css'; +import axios from 'axios'; +import { baseURLApi } from '../config'; +import { useRouter } from 'next/router'; +import ErrorBoundary from "../components/ErrorBoundary"; +import DevModeBadge from '../components/DevModeBadge'; + +// Initialize axios +axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API + ? process.env.NEXT_PUBLIC_BACK_API + : baseURLApi; + +axios.defaults.headers.common['Content-Type'] = 'application/json'; + +export type NextPageWithLayout

    , IP = P> = NextPage & { + getLayout?: (page: ReactElement) => ReactNode +} + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout +} + +function MyApp({ Component, pageProps }: AppPropsWithLayout) { + // Use the layout defined at the page level, if available + const getLayout = Component.getLayout || ((page) => page); + const router = useRouter(); + + axios.interceptors.request.use( + config => { + const token = localStorage.getItem('token'); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } else { + delete config.headers.Authorization; + } + + return config; + }, + error => { + return Promise.reject(error); + } + ); + + React.useEffect(() => { + // Setup message handler + const handleMessage = (event) => { + if (event.data === 'getLocation') { + event.source.postMessage( + { iframeLocation: window.location.pathname }, + event.origin, + ); + } + }; + + window.addEventListener('message', handleMessage); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, []); + + const title = 'meng-leap-cash' + const description = 'meng-leap-cash generated by Flatlogic' + const url = "https://flatlogic.com/" + const image = `https://flatlogic.com/logo.svg` + const imageWidth = '1920' + const imageHeight = '960' + + return ( + + {getLayout( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && } + + )} + + ) +} + +export default MyApp; diff --git a/frontend/src/pages/api/hello.js b/frontend/src/pages/api/hello.js new file mode 100644 index 0000000..f163396 --- /dev/null +++ b/frontend/src/pages/api/hello.js @@ -0,0 +1,5 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction + +export default function helloAPI(req, res) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/frontend/src/pages/api/logError.ts b/frontend/src/pages/api/logError.ts new file mode 100644 index 0000000..d72b3fd --- /dev/null +++ b/frontend/src/pages/api/logError.ts @@ -0,0 +1,83 @@ +import fsPromises from 'fs/promises'; +import path from 'path'; + +const dataFilePath = path.join(process.cwd(), 'json/runtimeError.json'); + +export default async function handler(req, res) { + // Ensure directory exists + try { + await fsPromises.mkdir(path.dirname(dataFilePath), { recursive: true }); + } catch (error) { + // Ignore if directory already exists + } + + if (req.method === 'GET') { + try { + // Check if file exists + try { + await fsPromises.access(dataFilePath); + } catch (error) { + // File doesn't exist, return empty object + return res.status(200).json({}); + } + + // Read the existing data from the JSON file + const jsonData = await fsPromises.readFile(dataFilePath, 'utf-8'); + + // Handle empty file + if (!jsonData || jsonData.trim() === '') { + // Write empty JSON object to file + await fsPromises.writeFile(dataFilePath, '{}', 'utf-8'); + return res.status(200).json({}); + } + + // Parse JSON data + try { + const objectData = JSON.parse(jsonData); + return res.status(200).json(objectData); + } catch (parseError) { + console.error('Error parsing JSON from file:', parseError); + // Reset the file with valid JSON if parsing fails + await fsPromises.writeFile(dataFilePath, '{}', 'utf-8'); + return res.status(200).json({}); + } + } catch (error) { + console.error('Error in GET handler:', error); + return res.status(200).json({}); // Return empty object instead of error + } + } else if (req.method === 'POST') { + try { + const updatedData = JSON.stringify(req.body); + + // Create directory if it doesn't exist + await fsPromises.mkdir(path.dirname(dataFilePath), { recursive: true }); + + // Write the updated data to the JSON file + await fsPromises.writeFile(dataFilePath, updatedData); + + // Send a success response + res.status(200).json({ message: 'Data stored successfully' }); + } catch (error) { + console.error('Error in POST handler:', error); + // Send an error response + res.status(500).json({ message: 'Error storing data' }); + } + } else if (req.method === 'DELETE') { + try { + // Create directory if it doesn't exist + await fsPromises.mkdir(path.dirname(dataFilePath), { recursive: true }); + + // Write empty JSON object to file + await fsPromises.writeFile(dataFilePath, '{}'); + + // Send a success response + res.status(200).json({message: 'Data deleted successfully'}); + } catch (error) { + console.error('Error in DELETE handler:', error); + // Send an error response + res.status(500).json({message: 'Error deleting data'}); + } + } else { + res.status(405).json({ message: 'Method not allowed' }); + } +} diff --git a/frontend/src/pages/branches/[branchesId].tsx b/frontend/src/pages/branches/[branchesId].tsx new file mode 100644 index 0000000..04d44fa --- /dev/null +++ b/frontend/src/pages/branches/[branchesId].tsx @@ -0,0 +1,135 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import dayjs from "dayjs"; + +import CardBox from '../../components/CardBox' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' + +import { Field, Form, Formik } from 'formik' +import FormField from '../../components/FormField' +import BaseDivider from '../../components/BaseDivider' +import BaseButtons from '../../components/BaseButtons' +import BaseButton from '../../components/BaseButton' +import FormCheckRadio from '../../components/FormCheckRadio' +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup' +import { SelectField } from "../../components/SelectField"; +import { SelectFieldMany } from "../../components/SelectFieldMany"; +import { SwitchField } from '../../components/SwitchField' +import {RichTextField} from "../../components/RichTextField"; + +import { update, fetch } from '../../stores/branches/branchesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' + +const EditBranches = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const initVals = { + + 'code': '', + + 'name': '', + + description: '', + + } + const [initialValues, setInitialValues] = useState(initVals) + + const { branches } = useAppSelector((state) => state.branches) + + const { branchesId } = router.query + + useEffect(() => { + dispatch(fetch({ id: branchesId })) + }, [branchesId]) + + useEffect(() => { + if (typeof branches === 'object') { + setInitialValues(branches) + } + }, [branches]) + + useEffect(() => { + if (typeof branches === 'object') { + + const newInitialVal = {...initVals}; + + Object.keys(initVals).forEach(el => newInitialVal[el] = (branches)[el]) + + setInitialValues(newInitialVal); + } + }, [branches]) + + const handleSubmit = async (data) => { + await dispatch(update({ id: branchesId, data })) + await router.push('/branches/branches-list') + } + + return ( + <> + + {getPageTitle('Edit branches')} + + + + {''} + + + handleSubmit(values)} + > +

    + + + + + + + + + + + + + + + + + + router.push('/branches/branches-list')}/> + + + + + + + ) +} + +EditBranches.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default EditBranches diff --git a/frontend/src/pages/branches/branches-edit.tsx b/frontend/src/pages/branches/branches-edit.tsx new file mode 100644 index 0000000..7e8d5b7 --- /dev/null +++ b/frontend/src/pages/branches/branches-edit.tsx @@ -0,0 +1,133 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import dayjs from "dayjs"; + +import CardBox from '../../components/CardBox' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' + +import { Field, Form, Formik } from 'formik' +import FormField from '../../components/FormField' +import BaseDivider from '../../components/BaseDivider' +import BaseButtons from '../../components/BaseButtons' +import BaseButton from '../../components/BaseButton' +import FormCheckRadio from '../../components/FormCheckRadio' +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup' +import { SelectField } from "../../components/SelectField"; +import { SelectFieldMany } from "../../components/SelectFieldMany"; +import { SwitchField } from '../../components/SwitchField' +import {RichTextField} from "../../components/RichTextField"; + +import { update, fetch } from '../../stores/branches/branchesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import dataFormatter from '../../helpers/dataFormatter'; + +const EditBranchesPage = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const initVals = { + + 'code': '', + + 'name': '', + + description: '', + + } + const [initialValues, setInitialValues] = useState(initVals) + + const { branches } = useAppSelector((state) => state.branches) + + const { id } = router.query + + useEffect(() => { + dispatch(fetch({ id: id })) + }, [id]) + + useEffect(() => { + if (typeof branches === 'object') { + setInitialValues(branches) + } + }, [branches]) + + useEffect(() => { + if (typeof branches === 'object') { + const newInitialVal = {...initVals}; + Object.keys(initVals).forEach(el => newInitialVal[el] = (branches)[el]) + setInitialValues(newInitialVal); + } + }, [branches]) + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })) + await router.push('/branches/branches-list') + } + + return ( + <> + + {getPageTitle('Edit branches')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + router.push('/branches/branches-list')}/> + + +
    +
    +
    + + ) +} + +EditBranchesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default EditBranchesPage diff --git a/frontend/src/pages/branches/branches-list.tsx b/frontend/src/pages/branches/branches-list.tsx new file mode 100644 index 0000000..aa87ec5 --- /dev/null +++ b/frontend/src/pages/branches/branches-list.tsx @@ -0,0 +1,128 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react' +import CardBox from '../../components/CardBox' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableBranches from '../../components/Branches/TableBranches' +import BaseButton from '../../components/BaseButton' +import axios from "axios"; +import {useAppDispatch, useAppSelector} from "../../stores/hooks"; +import CardBoxModal from "../../components/CardBoxModal"; +import DragDropFilePicker from "../../components/DragDropFilePicker"; +import {setRefetch, uploadCsv} from '../../stores/branches/branchesSlice'; + +const BranchesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{label: 'Code', title: 'code'},{label: 'Name', title: 'name'},{label: 'Description', title: 'description'}, + + ]); + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getBranchesCSV = async () => { + const response = await axios({url: '/branches?filetype=csv', method: 'GET',responseType: 'blob'}); + const type = response.headers['content-type'] + const blob = new Blob([response.data], { type: type }) + const link = document.createElement('a') + link.href = window.URL.createObjectURL(blob) + link.download = 'branchesCSV.csv' + link.click() + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Branches')} + + + + {''} + + + + + + setIsModalActive(true)} + /> +
    +
    +
    +
    + + + +
    + + + + + ) +} + +BranchesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default BranchesTablesPage diff --git a/frontend/src/pages/branches/branches-new.tsx b/frontend/src/pages/branches/branches-new.tsx new file mode 100644 index 0000000..ebbf0eb --- /dev/null +++ b/frontend/src/pages/branches/branches-new.tsx @@ -0,0 +1,106 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import CardBox from '../../components/CardBox' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' + +import { Field, Form, Formik } from 'formik' +import FormField from '../../components/FormField' +import BaseDivider from '../../components/BaseDivider' +import BaseButtons from '../../components/BaseButtons' +import BaseButton from '../../components/BaseButton' +import FormCheckRadio from '../../components/FormCheckRadio' +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup' +import { SwitchField } from '../../components/SwitchField' + +import { SelectField } from '../../components/SelectField' +import {RichTextField} from "../../components/RichTextField"; + +import { create } from '../../stores/branches/branchesSlice' +import { useAppDispatch } from '../../stores/hooks' +import { useRouter } from 'next/router' + +const initialValues = { + + code: '', + + name: '', + + description: '', + +} + +const BranchesNew = () => { + const router = useRouter() + const dispatch = useAppDispatch() + + const handleSubmit = async (data) => { + await dispatch(create(data)) + await router.push('/branches/branches-list') + } + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + router.push('/branches/branches-list')}/> + + +
    +
    +
    + + ) +} + +BranchesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default BranchesNew diff --git a/frontend/src/pages/branches/branches-table.tsx b/frontend/src/pages/branches/branches-table.tsx new file mode 100644 index 0000000..36483f9 --- /dev/null +++ b/frontend/src/pages/branches/branches-table.tsx @@ -0,0 +1,129 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react' +import CardBox from '../../components/CardBox' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableBranches from '../../components/Branches/TableBranches' +import BaseButton from '../../components/BaseButton' +import axios from "axios"; +import {useAppDispatch, useAppSelector} from "../../stores/hooks"; +import CardBoxModal from "../../components/CardBoxModal"; +import DragDropFilePicker from "../../components/DragDropFilePicker"; +import {setRefetch, uploadCsv} from '../../stores/branches/branchesSlice'; + +const BranchesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{label: 'Code', title: 'code'},{label: 'Name', title: 'name'},{label: 'Description', title: 'description'}, + + ]); + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getBranchesCSV = async () => { + const response = await axios({url: '/branches?filetype=csv', method: 'GET',responseType: 'blob'}); + const type = response.headers['content-type'] + const blob = new Blob([response.data], { type: type }) + const link = document.createElement('a') + link.href = window.URL.createObjectURL(blob) + link.download = 'branchesCSV.csv' + link.click() + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Branches')} + + + + {''} + + + + + + + setIsModalActive(true)} + /> +
    +
    +
    +
    + + + +
    + + + + + ) +} + +BranchesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default BranchesTablesPage diff --git a/frontend/src/pages/branches/branches-view.tsx b/frontend/src/pages/branches/branches-view.tsx new file mode 100644 index 0000000..5639272 --- /dev/null +++ b/frontend/src/pages/branches/branches-view.tsx @@ -0,0 +1,525 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head' +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import dayjs from "dayjs"; +import {useAppDispatch, useAppSelector} from "../../stores/hooks"; +import {useRouter} from "next/router"; +import { fetch } from '../../stores/branches/branchesSlice' +import dataFormatter from '../../helpers/dataFormatter'; +import LayoutAuthenticated from "../../layouts/Authenticated"; +import {getPageTitle} from "../../config"; +import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton"; +import SectionMain from "../../components/SectionMain"; +import CardBox from "../../components/CardBox"; +import BaseButton from "../../components/BaseButton"; +import BaseDivider from "../../components/BaseDivider"; +import {mdiChartTimelineVariant} from "@mdi/js"; +import {SwitchField} from "../../components/SwitchField"; +import FormField from "../../components/FormField"; + +const BranchesView = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const { branches } = useAppSelector((state) => state.branches) + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str,`str`) + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View branches')} + + + + + + + +
    +

    Code

    +

    {branches?.code}

    +
    + +
    +

    Name

    +

    {branches?.name}

    +
    + + +