Commit - 20250414-091321790

This commit is contained in:
Flatlogic Bot 2025-04-14 09:13:32 +00:00
parent f227081311
commit 4dbad9aa04
93 changed files with 6917 additions and 6611 deletions

View File

@ -1,3 +0,0 @@
backend/node_modules
frontend/node_modules
frontend/build

5
.gitignore vendored
View File

@ -1,5 +0,0 @@
/backend/node_modules
/frontend/node_modules
node_modules/
*/node_modules/
*/build/

View File

@ -1,26 +0,0 @@
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',
},
},
];

View File

@ -1,11 +0,0 @@
{
"singleQuote": true,
"tabWidth": 2,
"printWidth": 80,
"trailingComma": "all",
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always"
}

View File

@ -1,7 +0,0 @@
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")
};

View File

@ -1,23 +0,0 @@
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" ]

View File

@ -1,13 +0,0 @@
#test - template backend,
#### Run App on local machine:
##### Install local dependencies:
- `yarn install`
---
##### Start build:
- `yarn start`

View File

@ -1,42 +0,0 @@
{
"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"
}
}

View File

@ -1,42 +0,0 @@
const config = {
project_uuid: '19b1c1bd-792c-4fae-95df-528206c8b6ab',
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,
eventTypes: {
COMMIT: 'PROJECT_DEV_COMMIT',
COMMIT_STARTED: 'COMMIT_STARTED',
COMMIT_FAILED: 'COMMIT_FAILED',
COMMIT_COMPLETED: 'COMMIT_COMPLETED',
REPO_INIT_COMPLETED: 'REPO_INIT_COMPLETED',
REPO_INIT_STARTED: 'REPO_INIT_STARTED',
REPO_INIT_FAILED: 'REPO_INIT_FAILED',
REMOTE_REPO_CREATION_STARTED: 'REMOTE_REPO_CREATION_STARTED',
REMOTE_REPO_CREATION_COMPLETED: 'REMOTE_REPO_CREATION_COMPLETED',
REMOTE_REPO_CREATION_FAILED: 'REMOTE_REPO_CREATION_FAILED',
FETCH_RESET_STARTED: 'FETCH_RESET_STARTED',
FETCH_RESET_COMPLETED: 'FETCH_RESET_COMPLETED',
RESET_TO_BRANCH_COMPLETED: 'RESET_TO_BRANCH_COMPLETED',
RESET_TO_BRANCH_FAILED: 'RESET_TO_BRANCH_FAILED',
VALIDATION_ERROR: 'VALIDATION_ERROR',
DEPLOYMENT: 'DEPLOYMENT_STATUS',
BUILD_ERROR: 'BUILD_ERROR',
RUNTIME_ERROR: 'RUNTIME_ERROR',
USER_ACTION: 'USER_ACTION',
READ_PROJECT_TREE_STARTED: 'READ_PROJECT_TREE_STARTED',
READ_PROJECT_TREE_COMPLETED: 'READ_PROJECT_TREE_COMPLETED',
READ_PROJECT_TREE_FAILED: 'READ_PROJECT_TREE_FAILED',
FILE_VALIDATION_STARTED: 'FILE_VALIDATION_STARTED',
FILE_VALIDATION_COMPLETED: 'FILE_VALIDATION_COMPLETED',
}
};
module.exports = config;

View File

@ -1,23 +0,0 @@
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' });
}
};

View File

@ -1,54 +0,0 @@
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 = '30652';
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;

View File

@ -1,17 +0,0 @@
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;

View File

@ -1,8 +0,0 @@
function modifyPath(req, res, next) {
if (req.body && req.body.path) {
req.body.path = '../../../' + req.body.path;
}
next();
}
module.exports = modifyPath;

View File

@ -1,312 +0,0 @@
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;

View File

@ -1,40 +0,0 @@
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 } = req.body;
const result = await VSC.commitChanges(message, files);
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;

View File

@ -1,88 +0,0 @@
// 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();

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +0,0 @@
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;
}
};

View File

@ -1,16 +0,0 @@
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;
}
};

View File

@ -1,30 +0,0 @@
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;

View File

@ -1,100 +0,0 @@
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: `
<p>Hello,</p>
<p>You've been invited to {0} set password for your {1} account.</p>
<p><a href='{2}'>{2}</a></p>
<p>Thanks,</p>
<p>Your {0} team</p>
`,
},
emailAddressVerification: {
subject: `Verify your email for {0}`,
body: `
<p>Hello,</p>
<p>Follow this link to verify your email address.</p>
<p><a href='{0}'>{0}</a></p>
<p>If you didn't ask to verify this address, you can ignore this email.</p>
<p>Thanks,</p>
<p>Your {1} team</p>
`,
},
passwordReset: {
subject: `Reset your password for {0}`,
body: `
<p>Hello,</p>
<p>Follow this link to reset your {0} password for your {1} account.</p>
<p><a href='{2}'>{2}</a></p>
<p>If you didn't ask to reset your password, you can ignore this email.</p>
<p>Thanks,</p>
<p>Your {0} team</p>
`,
},
},
};
module.exports = errors;

View File

@ -1,67 +0,0 @@
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<object>} - 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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,374 @@
const db = require('../models');
const FileDBApi = require('./file');
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,
client_name: data.client_name || null,
date_registered: data.date_registered || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
},
{ transaction },
);
await clients.setCompanies(data.companies || null, {
transaction,
});
await clients.setClients_manager(data.clients_manager || null, {
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,
client_name: item.client_name || null,
date_registered: item.date_registered || 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 });
// For each item created, replace relation files
return clients;
}
static async update(id, data, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const globalAccess = currentUser.app_role?.globalAccess;
const clients = await db.clients.findByPk(id, {}, { transaction });
const updatePayload = {};
if (data.client_name !== undefined)
updatePayload.client_name = data.client_name;
if (data.date_registered !== undefined)
updatePayload.date_registered = data.date_registered;
updatePayload.updatedById = currentUser.id;
await clients.update(updatePayload, { transaction });
if (data.companies !== undefined) {
await clients.setCompanies(
data.companies,
{ transaction },
);
}
if (data.clients_manager !== undefined) {
await clients.setClients_manager(
data.clients_manager,
{ 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 });
output.companies = await clients.getCompanies({
transaction,
});
output.clients_manager = await clients.getClients_manager({
transaction,
});
return output;
}
static async findAll(filter, globalAccess, options) {
const limit = filter.limit || 0;
let offset = 0;
let where = {};
const currentPage = +filter.page;
const user = (options && options.currentUser) || null;
const userCompanies = (user && user.companies?.id) || null;
if (userCompanies) {
if (options?.currentUser?.companiesId) {
where.companiesId = options.currentUser.companiesId;
}
}
offset = currentPage * limit;
const orderBy = null;
const transaction = (options && options.transaction) || undefined;
let include = [
{
model: db.companies,
as: 'companies',
},
{
model: db.staff,
as: 'clients_manager',
where: filter.clients_manager
? {
[Op.or]: [
{
id: {
[Op.in]: filter.clients_manager
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
employee_name: {
[Op.or]: filter.clients_manager
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
if (filter) {
if (filter.id) {
where = {
...where,
['id']: Utils.uuid(filter.id),
};
}
if (filter.client_name) {
where = {
...where,
[Op.and]: Utils.ilike('clients', 'client_name', filter.client_name),
};
}
if (filter.date_registeredRange) {
const [start, end] = filter.date_registeredRange;
if (start !== undefined && start !== null && start !== '') {
where = {
...where,
date_registered: {
...where.date_registered,
[Op.gte]: start,
},
};
}
if (end !== undefined && end !== null && end !== '') {
where = {
...where,
date_registered: {
...where.date_registered,
[Op.lte]: end,
},
};
}
}
if (filter.active !== undefined) {
where = {
...where,
active: filter.active === true || filter.active === 'true',
};
}
if (filter.companies) {
const listItems = filter.companies.split('|').map((item) => {
return Utils.uuid(item);
});
where = {
...where,
companiesId: { [Op.or]: listItems },
};
}
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,
},
};
}
}
}
if (globalAccess) {
delete where.companiesId;
}
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,
globalAccess,
organizationId,
) {
let where = {};
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
}
if (query) {
where = {
[Op.or]: [
{ ['id']: Utils.uuid(query) },
Utils.ilike('clients', 'date_registered', query),
],
};
}
const records = await db.clients.findAll({
attributes: ['id', 'date_registered'],
where,
limit: limit ? Number(limit) : undefined,
offset: offset ? Number(offset) : undefined,
orderBy: [['date_registered', 'ASC']],
});
return records.map((record) => ({
id: record.id,
label: record.date_registered,
}));
}
};

View File

@ -162,6 +162,14 @@ module.exports = class CompaniesDBApi {
transaction,
});
output.staff_companies = await companies.getStaff_companies({
transaction,
});
output.clients_companies = await companies.getClients_companies({
transaction,
});
return output;
}

307
backend/src/db/api/staff.js Normal file
View File

@ -0,0 +1,307 @@
const db = require('../models');
const FileDBApi = require('./file');
const crypto = require('crypto');
const Utils = require('../utils');
const Sequelize = db.Sequelize;
const Op = Sequelize.Op;
module.exports = class StaffDBApi {
static async create(data, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const staff = await db.staff.create(
{
id: data.id || undefined,
employee_name: data.employee_name || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
},
{ transaction },
);
await staff.setCompanies(data.companies || null, {
transaction,
});
return staff;
}
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 staffData = data.map((item, index) => ({
id: item.id || undefined,
employee_name: item.employee_name || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
createdAt: new Date(Date.now() + index * 1000),
}));
// Bulk create items
const staff = await db.staff.bulkCreate(staffData, { transaction });
// For each item created, replace relation files
return staff;
}
static async update(id, data, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const globalAccess = currentUser.app_role?.globalAccess;
const staff = await db.staff.findByPk(id, {}, { transaction });
const updatePayload = {};
if (data.employee_name !== undefined)
updatePayload.employee_name = data.employee_name;
updatePayload.updatedById = currentUser.id;
await staff.update(updatePayload, { transaction });
if (data.companies !== undefined) {
await staff.setCompanies(
data.companies,
{ transaction },
);
}
return staff;
}
static async deleteByIds(ids, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const staff = await db.staff.findAll({
where: {
id: {
[Op.in]: ids,
},
},
transaction,
});
await db.sequelize.transaction(async (transaction) => {
for (const record of staff) {
await record.update({ deletedBy: currentUser.id }, { transaction });
}
for (const record of staff) {
await record.destroy({ transaction });
}
});
return staff;
}
static async remove(id, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const staff = await db.staff.findByPk(id, options);
await staff.update(
{
deletedBy: currentUser.id,
},
{
transaction,
},
);
await staff.destroy({
transaction,
});
return staff;
}
static async findBy(where, options) {
const transaction = (options && options.transaction) || undefined;
const staff = await db.staff.findOne({ where }, { transaction });
if (!staff) {
return staff;
}
const output = staff.get({ plain: true });
output.clients_clients_manager = await staff.getClients_clients_manager({
transaction,
});
output.companies = await staff.getCompanies({
transaction,
});
return output;
}
static async findAll(filter, globalAccess, options) {
const limit = filter.limit || 0;
let offset = 0;
let where = {};
const currentPage = +filter.page;
const user = (options && options.currentUser) || null;
const userCompanies = (user && user.companies?.id) || null;
if (userCompanies) {
if (options?.currentUser?.companiesId) {
where.companiesId = options.currentUser.companiesId;
}
}
offset = currentPage * limit;
const orderBy = null;
const transaction = (options && options.transaction) || undefined;
let include = [
{
model: db.companies,
as: 'companies',
},
];
if (filter) {
if (filter.id) {
where = {
...where,
['id']: Utils.uuid(filter.id),
};
}
if (filter.employee_name) {
where = {
...where,
[Op.and]: Utils.ilike('staff', 'employee_name', filter.employee_name),
};
}
if (filter.active !== undefined) {
where = {
...where,
active: filter.active === true || filter.active === 'true',
};
}
if (filter.companies) {
const listItems = filter.companies.split('|').map((item) => {
return Utils.uuid(item);
});
where = {
...where,
companiesId: { [Op.or]: listItems },
};
}
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,
},
};
}
}
}
if (globalAccess) {
delete where.companiesId;
}
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.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,
globalAccess,
organizationId,
) {
let where = {};
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
}
if (query) {
where = {
[Op.or]: [
{ ['id']: Utils.uuid(query) },
Utils.ilike('staff', 'employee_name', query),
],
};
}
const records = await db.staff.findAll({
attributes: ['id', 'employee_name'],
where,
limit: limit ? Number(limit) : undefined,
offset: offset ? Number(offset) : undefined,
orderBy: [['employee_name', 'ASC']],
});
return records.map((record) => ({
id: record.id,
label: record.employee_name,
}));
}
};

View File

@ -279,11 +279,6 @@ module.exports = class UsersDBApi {
const output = users.get({ plain: true });
output.work_orders_production_manager =
await users.getWork_orders_production_manager({
transaction,
});
output.avatar = await users.getAvatar({
transaction,
});

View File

@ -25,10 +25,6 @@ module.exports = class Work_ordersDBApi {
{ transaction },
);
await work_orders.setProduction_manager(data.production_manager || null, {
transaction,
});
await work_orders.setCompanies(data.companies || null, {
transaction,
});
@ -92,14 +88,6 @@ module.exports = class Work_ordersDBApi {
await work_orders.update(updatePayload, { transaction });
if (data.production_manager !== undefined) {
await work_orders.setProduction_manager(
data.production_manager,
{ transaction },
);
}
if (data.companies !== undefined) {
await work_orders.setCompanies(
data.companies,
@ -185,10 +173,6 @@ module.exports = class Work_ordersDBApi {
transaction,
});
output.production_manager = await work_orders.getProduction_manager({
transaction,
});
output.raw_materials = await work_orders.getRaw_materials({
transaction,
});
@ -226,32 +210,6 @@ module.exports = class Work_ordersDBApi {
const transaction = (options && options.transaction) || undefined;
let include = [
{
model: db.users,
as: 'production_manager',
where: filter.production_manager
? {
[Op.or]: [
{
id: {
[Op.in]: filter.production_manager
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
firstName: {
[Op.or]: filter.production_manager
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
{
model: db.companies,
as: 'companies',

View File

@ -0,0 +1,90 @@
module.exports = {
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async up(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.createTable(
'staff',
{
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(
'staff',
'companiesId',
{
type: Sequelize.DataTypes.UUID,
references: {
model: 'companies',
key: 'id',
},
},
{ transaction },
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async down(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeColumn('staff', 'companiesId', {
transaction,
});
await queryInterface.dropTable('staff', { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,49 @@
module.exports = {
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async up(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.addColumn(
'staff',
'employee_name',
{
type: Sequelize.DataTypes.TEXT,
},
{ transaction },
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async down(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeColumn('staff', 'employee_name', {
transaction,
});
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,90 @@
module.exports = {
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async up(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
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.addColumn(
'clients',
'companiesId',
{
type: Sequelize.DataTypes.UUID,
references: {
model: 'companies',
key: 'id',
},
},
{ transaction },
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async down(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeColumn('clients', 'companiesId', {
transaction,
});
await queryInterface.dropTable('clients', { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,49 @@
module.exports = {
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async up(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.addColumn(
'clients',
'client_name',
{
type: Sequelize.DataTypes.TEXT,
},
{ transaction },
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async down(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeColumn('clients', 'client_name', {
transaction,
});
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,49 @@
module.exports = {
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async up(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.addColumn(
'clients',
'date_registered',
{
type: Sequelize.DataTypes.DATE,
},
{ transaction },
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async down(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeColumn('clients', 'date_registered', {
transaction,
});
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,54 @@
module.exports = {
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async up(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.addColumn(
'clients',
'clients_managerId',
{
type: Sequelize.DataTypes.UUID,
references: {
model: 'staff',
key: 'id',
},
},
{ transaction },
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async down(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeColumn('clients', 'clients_managerId', {
transaction,
});
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,54 @@
module.exports = {
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async up(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.removeColumn('work_orders', 'production_managerId', {
transaction,
});
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
/**
* @param {QueryInterface} queryInterface
* @param {Sequelize} Sequelize
* @returns {Promise<void>}
*/
async down(queryInterface, Sequelize) {
/**
* @type {Transaction}
*/
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.addColumn(
'work_orders',
'production_managerId',
{
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
{ transaction },
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,69 @@
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,
},
client_name: {
type: DataTypes.TEXT,
},
date_registered: {
type: DataTypes.DATE,
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: true,
paranoid: true,
freezeTableName: true,
},
);
clients.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
db.clients.belongsTo(db.companies, {
as: 'companies',
foreignKey: {
name: 'companiesId',
},
constraints: false,
});
db.clients.belongsTo(db.staff, {
as: 'clients_manager',
foreignKey: {
name: 'clients_managerId',
},
constraints: false,
});
db.clients.belongsTo(db.users, {
as: 'createdBy',
});
db.clients.belongsTo(db.users, {
as: 'updatedBy',
});
};
return clients;
};

View File

@ -98,6 +98,22 @@ module.exports = function (sequelize, DataTypes) {
constraints: false,
});
db.companies.hasMany(db.staff, {
as: 'staff_companies',
foreignKey: {
name: 'companiesId',
},
constraints: false,
});
db.companies.hasMany(db.clients, {
as: 'clients_companies',
foreignKey: {
name: 'companiesId',
},
constraints: false,
});
//end loop
db.companies.belongsTo(db.users, {

View File

@ -0,0 +1,65 @@
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 = sequelize.define(
'staff',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
employee_name: {
type: DataTypes.TEXT,
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: true,
paranoid: true,
freezeTableName: true,
},
);
staff.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
db.staff.hasMany(db.clients, {
as: 'clients_clients_manager',
foreignKey: {
name: 'clients_managerId',
},
constraints: false,
});
//end loop
db.staff.belongsTo(db.companies, {
as: 'companies',
foreignKey: {
name: 'companiesId',
},
constraints: false,
});
db.staff.belongsTo(db.users, {
as: 'createdBy',
});
db.staff.belongsTo(db.users, {
as: 'updatedBy',
});
};
return staff;
};

View File

@ -102,14 +102,6 @@ module.exports = function (sequelize, DataTypes) {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
db.users.hasMany(db.work_orders, {
as: 'work_orders_production_manager',
foreignKey: {
name: 'production_managerId',
},
constraints: false,
});
//end loop
db.users.belongsTo(db.roles, {

View File

@ -88,14 +88,6 @@ module.exports = function (sequelize, DataTypes) {
//end loop
db.work_orders.belongsTo(db.users, {
as: 'production_manager',
foreignKey: {
name: 'production_managerId',
},
constraints: false,
});
db.work_orders.belongsTo(db.companies, {
as: 'companies',
foreignKey: {

View File

@ -120,6 +120,8 @@ module.exports = {
'roles',
'permissions',
'companies',
'staff',
'clients',
,
];
await queryInterface.bulkInsert(
@ -969,6 +971,56 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_WORK_ORDERS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('CREATE_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('READ_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('UPDATE_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('DELETE_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('CREATE_CLIENTS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('READ_CLIENTS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('UPDATE_CLIENTS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('DELETE_CLIENTS'),
},
{
createdAt,
updatedAt,
@ -1244,6 +1296,56 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_COMPANIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('CREATE_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('READ_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('UPDATE_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('DELETE_STAFF'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('CREATE_CLIENTS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('READ_CLIENTS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('UPDATE_CLIENTS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('DELETE_CLIENTS'),
},
{
createdAt,
updatedAt,

View File

@ -17,6 +17,10 @@ const WorkOrders = db.work_orders;
const Companies = db.companies;
const Staff = db.staff;
const Clients = db.clients;
const EmployeesData = [
{
employee_name: 'John Doe',
@ -61,7 +65,7 @@ const InventoryData = [
quantity: 150,
status: 'returned',
status: 'available',
// type code here for "relation_one" field
},
@ -71,7 +75,7 @@ const InventoryData = [
quantity: 75,
status: 'available',
status: 'reserved',
// type code here for "relation_one" field
},
@ -81,7 +85,7 @@ const InventoryData = [
quantity: 200,
status: 'reserved',
status: 'available',
// type code here for "relation_one" field
},
@ -231,8 +235,6 @@ const WorkOrdersData = [
{
order_number: 'WO-001',
// type code here for "relation_one" field
// type code here for "relation_many" field
// type code here for "relation_many" field
@ -247,8 +249,6 @@ const WorkOrdersData = [
{
order_number: 'WO-002',
// type code here for "relation_one" field
// type code here for "relation_many" field
// type code here for "relation_many" field
@ -263,8 +263,6 @@ const WorkOrdersData = [
{
order_number: 'WO-003',
// type code here for "relation_one" field
// type code here for "relation_many" field
// type code here for "relation_many" field
@ -291,6 +289,58 @@ const CompaniesData = [
},
];
const StaffData = [
{
// type code here for "relation_one" field
employee_name: 'Albrecht von Haller',
},
{
// type code here for "relation_one" field
employee_name: 'Alfred Binet',
},
{
// type code here for "relation_one" field
employee_name: 'Jonas Salk',
},
];
const ClientsData = [
{
// type code here for "relation_one" field
client_name: 'Alfred Binet',
date_registered: new Date(Date.now()),
// type code here for "relation_one" field
},
{
// type code here for "relation_one" field
client_name: 'Albert Einstein',
date_registered: new Date(Date.now()),
// type code here for "relation_one" field
},
{
// type code here for "relation_one" field
client_name: 'Arthur Eddington',
date_registered: new Date(Date.now()),
// type code here for "relation_one" field
},
];
// Similar logic for "relation_many"
async function associateUserWithCompany() {
@ -575,41 +625,6 @@ async function associateSupplierWithCompany() {
}
}
async function associateWorkOrderWithProduction_manager() {
const relatedProduction_manager0 = await Users.findOne({
offset: Math.floor(Math.random() * (await Users.count())),
});
const WorkOrder0 = await WorkOrders.findOne({
order: [['id', 'ASC']],
offset: 0,
});
if (WorkOrder0?.setProduction_manager) {
await WorkOrder0.setProduction_manager(relatedProduction_manager0);
}
const relatedProduction_manager1 = await Users.findOne({
offset: Math.floor(Math.random() * (await Users.count())),
});
const WorkOrder1 = await WorkOrders.findOne({
order: [['id', 'ASC']],
offset: 1,
});
if (WorkOrder1?.setProduction_manager) {
await WorkOrder1.setProduction_manager(relatedProduction_manager1);
}
const relatedProduction_manager2 = await Users.findOne({
offset: Math.floor(Math.random() * (await Users.count())),
});
const WorkOrder2 = await WorkOrders.findOne({
order: [['id', 'ASC']],
offset: 2,
});
if (WorkOrder2?.setProduction_manager) {
await WorkOrder2.setProduction_manager(relatedProduction_manager2);
}
}
// Similar logic for "relation_many"
// Similar logic for "relation_many"
@ -649,6 +664,111 @@ async function associateWorkOrderWithCompany() {
}
}
async function associateStaffWithCompany() {
const relatedCompany0 = await Companies.findOne({
offset: Math.floor(Math.random() * (await Companies.count())),
});
const Staff0 = await Staff.findOne({
order: [['id', 'ASC']],
offset: 0,
});
if (Staff0?.setCompany) {
await Staff0.setCompany(relatedCompany0);
}
const relatedCompany1 = await Companies.findOne({
offset: Math.floor(Math.random() * (await Companies.count())),
});
const Staff1 = await Staff.findOne({
order: [['id', 'ASC']],
offset: 1,
});
if (Staff1?.setCompany) {
await Staff1.setCompany(relatedCompany1);
}
const relatedCompany2 = await Companies.findOne({
offset: Math.floor(Math.random() * (await Companies.count())),
});
const Staff2 = await Staff.findOne({
order: [['id', 'ASC']],
offset: 2,
});
if (Staff2?.setCompany) {
await Staff2.setCompany(relatedCompany2);
}
}
async function associateClientWithCompany() {
const relatedCompany0 = await Companies.findOne({
offset: Math.floor(Math.random() * (await Companies.count())),
});
const Client0 = await Clients.findOne({
order: [['id', 'ASC']],
offset: 0,
});
if (Client0?.setCompany) {
await Client0.setCompany(relatedCompany0);
}
const relatedCompany1 = await Companies.findOne({
offset: Math.floor(Math.random() * (await Companies.count())),
});
const Client1 = await Clients.findOne({
order: [['id', 'ASC']],
offset: 1,
});
if (Client1?.setCompany) {
await Client1.setCompany(relatedCompany1);
}
const relatedCompany2 = await Companies.findOne({
offset: Math.floor(Math.random() * (await Companies.count())),
});
const Client2 = await Clients.findOne({
order: [['id', 'ASC']],
offset: 2,
});
if (Client2?.setCompany) {
await Client2.setCompany(relatedCompany2);
}
}
async function associateClientWithClients_manager() {
const relatedClients_manager0 = await Staff.findOne({
offset: Math.floor(Math.random() * (await Staff.count())),
});
const Client0 = await Clients.findOne({
order: [['id', 'ASC']],
offset: 0,
});
if (Client0?.setClients_manager) {
await Client0.setClients_manager(relatedClients_manager0);
}
const relatedClients_manager1 = await Staff.findOne({
offset: Math.floor(Math.random() * (await Staff.count())),
});
const Client1 = await Clients.findOne({
order: [['id', 'ASC']],
offset: 1,
});
if (Client1?.setClients_manager) {
await Client1.setClients_manager(relatedClients_manager1);
}
const relatedClients_manager2 = await Staff.findOne({
offset: Math.floor(Math.random() * (await Staff.count())),
});
const Client2 = await Clients.findOne({
order: [['id', 'ASC']],
offset: 2,
});
if (Client2?.setClients_manager) {
await Client2.setClients_manager(relatedClients_manager2);
}
}
module.exports = {
up: async (queryInterface, Sequelize) => {
await Employees.bulkCreate(EmployeesData);
@ -667,6 +787,10 @@ module.exports = {
await Companies.bulkCreate(CompaniesData);
await Staff.bulkCreate(StaffData);
await Clients.bulkCreate(ClientsData);
await Promise.all([
// Similar logic for "relation_many"
@ -688,13 +812,17 @@ module.exports = {
await associateSupplierWithCompany(),
await associateWorkOrderWithProduction_manager(),
// Similar logic for "relation_many"
// Similar logic for "relation_many"
await associateWorkOrderWithCompany(),
await associateStaffWithCompany(),
await associateClientWithCompany(),
await associateClientWithClients_manager(),
]);
},
@ -714,5 +842,9 @@ module.exports = {
await queryInterface.bulkDelete('work_orders', null, {});
await queryInterface.bulkDelete('companies', null, {});
await queryInterface.bulkDelete('staff', null, {});
await queryInterface.bulkDelete('clients', null, {});
},
};

View File

@ -0,0 +1,87 @@
const { v4: uuid } = require('uuid');
const db = require('../models');
const Sequelize = require('sequelize');
const config = require('../../config');
module.exports = {
/**
* @param{import("sequelize").QueryInterface} queryInterface
* @return {Promise<void>}
*/
async up(queryInterface) {
const createdAt = new Date();
const updatedAt = new Date();
/** @type {Map<string, string>} */
const idMap = new Map();
/**
* @param {string} key
* @return {string}
*/
function getId(key) {
if (idMap.has(key)) {
return idMap.get(key);
}
const id = uuid();
idMap.set(key, id);
return id;
}
/**
* @param {string} name
*/
function createPermissions(name) {
return [
{
id: getId(`CREATE_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `CREATE_${name.toUpperCase()}`,
},
{
id: getId(`READ_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `READ_${name.toUpperCase()}`,
},
{
id: getId(`UPDATE_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `UPDATE_${name.toUpperCase()}`,
},
{
id: getId(`DELETE_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `DELETE_${name.toUpperCase()}`,
},
];
}
const entities = ['staff'];
const createdPermissions = entities.flatMap(createPermissions);
// Add permissions to database
await queryInterface.bulkInsert('permissions', createdPermissions);
// Get permissions ids
const permissionsIds = createdPermissions.map((p) => p.id);
// Get admin role
const adminRole = await db.roles.findOne({
where: { name: config.roles.super_admin },
});
if (adminRole) {
// Add permissions to admin role if it exists
await adminRole.addPermissions(permissionsIds);
}
},
down: async (queryInterface, Sequelize) => {
await queryInterface.bulkDelete(
'permissions',
entities.flatMap(createPermissions),
);
},
};

View File

@ -0,0 +1,87 @@
const { v4: uuid } = require('uuid');
const db = require('../models');
const Sequelize = require('sequelize');
const config = require('../../config');
module.exports = {
/**
* @param{import("sequelize").QueryInterface} queryInterface
* @return {Promise<void>}
*/
async up(queryInterface) {
const createdAt = new Date();
const updatedAt = new Date();
/** @type {Map<string, string>} */
const idMap = new Map();
/**
* @param {string} key
* @return {string}
*/
function getId(key) {
if (idMap.has(key)) {
return idMap.get(key);
}
const id = uuid();
idMap.set(key, id);
return id;
}
/**
* @param {string} name
*/
function createPermissions(name) {
return [
{
id: getId(`CREATE_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `CREATE_${name.toUpperCase()}`,
},
{
id: getId(`READ_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `READ_${name.toUpperCase()}`,
},
{
id: getId(`UPDATE_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `UPDATE_${name.toUpperCase()}`,
},
{
id: getId(`DELETE_${name.toUpperCase()}`),
createdAt,
updatedAt,
name: `DELETE_${name.toUpperCase()}`,
},
];
}
const entities = ['clients'];
const createdPermissions = entities.flatMap(createPermissions);
// Add permissions to database
await queryInterface.bulkInsert('permissions', createdPermissions);
// Get permissions ids
const permissionsIds = createdPermissions.map((p) => p.id);
// Get admin role
const adminRole = await db.roles.findOne({
where: { name: config.roles.super_admin },
});
if (adminRole) {
// Add permissions to admin role if it exists
await adminRole.addPermissions(permissionsIds);
}
},
down: async (queryInterface, Sequelize) => {
await queryInterface.bulkDelete(
'permissions',
entities.flatMap(createPermissions),
);
},
};

View File

@ -43,6 +43,10 @@ const permissionsRoutes = require('./routes/permissions');
const companiesRoutes = require('./routes/companies');
const staffRoutes = require('./routes/staff');
const clientsRoutes = require('./routes/clients');
const getBaseUrl = (url) => {
if (!url) return '';
return url.endsWith('/api') ? url.slice(0, -4) : url;
@ -174,6 +178,18 @@ app.use(
companiesRoutes,
);
app.use(
'/api/staff',
passport.authenticate('jwt', { session: false }),
staffRoutes,
);
app.use(
'/api/clients',
passport.authenticate('jwt', { session: false }),
clientsRoutes,
);
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),

View File

@ -0,0 +1,452 @@
const express = require('express');
const ClientsService = require('../services/clients');
const ClientsDBApi = require('../db/api/clients');
const wrapAsync = require('../helpers').wrapAsync;
const config = require('../config');
const router = express.Router();
const { parse } = require('json2csv');
const { checkCrudPermissions } = require('../middlewares/check-permissions');
router.use(checkCrudPermissions('clients'));
/**
* @swagger
* components:
* schemas:
* Clients:
* type: object
* properties:
* client_name:
* type: string
* default: client_name
*/
/**
* @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 globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await ClientsDBApi.findAll(req.query, globalAccess, {
currentUser,
});
if (filetype && filetype === 'csv') {
const fields = ['id', 'client_name', 'date_registered'];
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 globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await ClientsDBApi.findAll(req.query, globalAccess, {
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 globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id;
const payload = await ClientsDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
globalAccess,
organizationId,
);
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;

447
backend/src/routes/staff.js Normal file
View File

@ -0,0 +1,447 @@
const express = require('express');
const StaffService = require('../services/staff');
const StaffDBApi = require('../db/api/staff');
const wrapAsync = require('../helpers').wrapAsync;
const config = require('../config');
const router = express.Router();
const { parse } = require('json2csv');
const { checkCrudPermissions } = require('../middlewares/check-permissions');
router.use(checkCrudPermissions('staff'));
/**
* @swagger
* components:
* schemas:
* Staff:
* type: object
* properties:
* employee_name:
* type: string
* default: employee_name
*/
/**
* @swagger
* tags:
* name: Staff
* description: The Staff managing API
*/
/**
* @swagger
* /api/staff:
* post:
* security:
* - bearerAuth: []
* tags: [Staff]
* 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"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Staff"
* 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 StaffService.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]
* 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"
* responses:
* 200:
* description: The items were successfully imported
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Staff"
* 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 StaffService.bulkImport(req, res, true, link.host);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/staff/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Staff]
* 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"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Staff"
* 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 StaffService.update(req.body.data, req.body.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/staff/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Staff]
* 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"
* 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 StaffService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/staff/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Staff]
* 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"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post(
'/deleteByIds',
wrapAsync(async (req, res) => {
await StaffService.deleteByIds(req.body.data, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/staff:
* get:
* security:
* - bearerAuth: []
* tags: [Staff]
* summary: Get all staff
* description: Get all staff
* responses:
* 200:
* description: Staff list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Staff"
* 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 globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await StaffDBApi.findAll(req.query, globalAccess, {
currentUser,
});
if (filetype && filetype === 'csv') {
const fields = ['id', 'employee_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/staff/count:
* get:
* security:
* - bearerAuth: []
* tags: [Staff]
* summary: Count all staff
* description: Count all staff
* responses:
* 200:
* description: Staff count successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Staff"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get(
'/count',
wrapAsync(async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const currentUser = req.currentUser;
const payload = await StaffDBApi.findAll(req.query, globalAccess, {
countOnly: true,
currentUser,
});
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/staff/autocomplete:
* get:
* security:
* - bearerAuth: []
* tags: [Staff]
* summary: Find all staff that match search criteria
* description: Find all staff that match search criteria
* responses:
* 200:
* description: Staff list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Staff"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id;
const payload = await StaffDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
globalAccess,
organizationId,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/staff/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Staff]
* 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"
* 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 StaffDBApi.findBy({ id: req.params.id });
res.status(200).send(payload);
}),
);
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

View File

@ -0,0 +1,114 @@
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;
}
}
};

View File

@ -58,6 +58,10 @@ module.exports = class SearchService {
work_orders: ['order_number'],
companies: ['name'],
staff: ['employee_name'],
clients: ['client_name'],
};
const columnsInt = {
employees: ['payroll'],

View File

@ -0,0 +1,114 @@
const db = require('../db/models');
const StaffDBApi = require('../db/api/staff');
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 StaffService {
static async create(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await StaffDBApi.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 StaffDBApi.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 = await StaffDBApi.findBy({ id }, { transaction });
if (!staff) {
throw new ValidationError('staffNotFound');
}
const updatedStaff = await StaffDBApi.update(id, data, {
currentUser,
transaction,
});
await transaction.commit();
return updatedStaff;
} catch (error) {
await transaction.rollback();
throw error;
}
}
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await StaffDBApi.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 StaffDBApi.remove(id, {
currentUser,
transaction,
});
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
};

View File

@ -0,0 +1,133 @@
import React from 'react';
import ImageField from '../ImageField';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import dataFormatter from '../../helpers/dataFormatter';
import { Pagination } from '../Pagination';
import { saveFile } from '../../helpers/fileSaver';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link';
import { hasPermission } from '../../helpers/userPermissions';
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);
const currentUser = useAppSelector((state) => state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_CLIENTS');
return (
<div className={'p-4'}>
{loading && <LoadingSpinner />}
<ul
role='list'
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
>
{!loading &&
clients.map((item, index) => (
<li
key={item.id}
className={`overflow-hidden ${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} border ${focusRing} border-gray-200 dark:border-dark-700 ${
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
>
<div
className={`flex items-center ${bgColor} p-6 gap-x-4 border-b border-gray-900/5 bg-gray-50 dark:bg-dark-800 relative`}
>
<Link
href={`/clients/clients-view/?id=${item.id}`}
className='text-lg font-bold leading-6 line-clamp-1'
>
{item.date_registered}
</Link>
<div className='ml-auto '>
<ListActionsPopover
onDelete={onDelete}
itemId={item.id}
pathEdit={`/clients/clients-edit/?id=${item.id}`}
pathView={`/clients/clients-view/?id=${item.id}`}
hasUpdatePermission={hasUpdatePermission}
/>
</div>
</div>
<dl className='divide-y divide-stone-300 dark:divide-dark-700 px-6 py-4 text-sm leading-6 h-64 overflow-y-auto'>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>
Client name
</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{item.client_name}
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>
Date registered
</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{dataFormatter.dateTimeFormatter(item.date_registered)}
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>
Clients_manager
</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{dataFormatter.staffOneListFormatter(
item.clients_manager,
)}
</div>
</dd>
</div>
</dl>
</li>
))}
{!loading && clients.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
</div>
)}
</ul>
<div className={'flex items-center justify-center my-6'}>
<Pagination
currentPage={currentPage}
numPages={numPages}
setCurrentPage={onPageChange}
/>
</div>
</div>
);
};
export default CardClients;

View File

@ -0,0 +1,110 @@
import React from 'react';
import CardBox from '../CardBox';
import ImageField from '../ImageField';
import dataFormatter from '../../helpers/dataFormatter';
import { saveFile } from '../../helpers/fileSaver';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import { Pagination } from '../Pagination';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link';
import { hasPermission } from '../../helpers/userPermissions';
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 currentUser = useAppSelector((state) => state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_CLIENTS');
const corners = useAppSelector((state) => state.style.corners);
const bgColor = useAppSelector((state) => state.style.cardsColor);
return (
<>
<div className='relative overflow-x-auto p-4 space-y-4'>
{loading && <LoadingSpinner />}
{!loading &&
clients.map((item) => (
<CardBox
hasTable
isList
key={item.id}
className={'rounded shadow-none'}
>
<div
className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}
>
<Link
href={`/clients/clients-view/?id=${item.id}`}
className={
'flex-1 px-4 py-6 h-24 flex divide-x-2 divide-stone-300 items-center overflow-hidden`}> dark:divide-dark-700 overflow-x-auto'
}
>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Client name</p>
<p className={'line-clamp-2'}>{item.client_name}</p>
</div>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>
Date registered
</p>
<p className={'line-clamp-2'}>
{dataFormatter.dateTimeFormatter(item.date_registered)}
</p>
</div>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>
Clients_manager
</p>
<p className={'line-clamp-2'}>
{dataFormatter.staffOneListFormatter(
item.clients_manager,
)}
</p>
</div>
</Link>
<ListActionsPopover
onDelete={onDelete}
itemId={item.id}
pathEdit={`/clients/clients-edit/?id=${item.id}`}
pathView={`/clients/clients-view/?id=${item.id}`}
hasUpdatePermission={hasUpdatePermission}
/>
</div>
</CardBox>
))}
{!loading && clients.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
</div>
)}
</div>
<div className={'flex items-center justify-center my-6'}>
<Pagination
currentPage={currentPage}
numPages={numPages}
setCurrentPage={onPageChange}
/>
</div>
</>
);
};
export default ListClients;

View File

@ -0,0 +1,481 @@
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';
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<GridColDef[]>([]);
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(() => {
if (!currentUser) return;
loadColumns(handleDeleteModalAction, `clients`, currentUser).then(
(newCols) => setColumns(newCols),
);
}, [currentUser]);
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 rounded dark:placeholder-gray-400 ' +
` ${bgColor} ${focusRing} ${corners} ` +
'dark:bg-slate-800 border';
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `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);
}}
/>
</div>
);
return (
<>
{filterItems && Array.isArray(filterItems) && filterItems.length ? (
<CardBox>
<Formik
initialValues={{
checkboxes: ['lorem'],
switches: ['lorem'],
radio: 'lorem',
}}
onSubmit={() => null}
>
<Form>
<>
{filterItems &&
filterItems.map((filterItem) => {
return (
<div key={filterItem.id} className='flex mb-4'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
Filter
</div>
<Field
className={controlClasses}
name='selectedField'
id='selectedField'
component='select'
value={filterItem?.fields?.selectedField || ''}
onChange={handleChange(filterItem.id)}
>
{filters.map((selectOption) => (
<option
key={selectOption.title}
value={`${selectOption.title}`}
>
{selectOption.label}
</option>
))}
</Field>
</div>
{filters.find(
(filter) =>
filter.title === filterItem?.fields?.selectedField,
)?.type === 'enum' ? (
<div className='flex flex-col w-full mr-3'>
<div className='text-gray-500 font-bold'>Value</div>
<Field
className={controlClasses}
name='filterValue'
id='filterValue'
component='select'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value=''>Select Value</option>
{filters
.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField,
)
?.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Field>
</div>
) : filters.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField,
)?.number ? (
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
value={
filterItem?.fields?.filterValueFrom || ''
}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>
To
</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : filters.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField,
)?.date ? (
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
type='datetime-local'
value={
filterItem?.fields?.filterValueFrom || ''
}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>
To
</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : (
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
Contains
</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
)}
<div className='flex flex-col'>
<div className=' text-gray-500 font-bold'>
Action
</div>
<BaseButton
className='my-2'
type='reset'
color='danger'
label='Delete'
onClick={() => {
deleteFilter(filterItem.id);
}}
/>
</div>
</div>
);
})}
<div className='flex'>
<BaseButton
className='my-2 mr-3'
color='success'
label='Apply'
onClick={handleSubmit}
/>
<BaseButton
className='my-2'
color='info'
label='Cancel'
onClick={handleReset}
/>
</div>
</>
</Form>
</Formik>
</CardBox>
) : null}
<CardBoxModal
title='Please confirm'
buttonColor='info'
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
</CardBoxModal>
{dataGrid}
{selectedRows.length > 0 &&
createPortal(
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),
)}
<ToastContainer />
</>
);
};
export default TableSampleClients;

View File

@ -0,0 +1,109 @@
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 ImageField from '../ImageField';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import DataGridMultiSelect from '../DataGridMultiSelect';
import ListActionsPopover from '../ListActionsPopover';
import { hasPermission } from '../../helpers/userPermissions';
type Params = (id: string) => void;
export const loadColumns = async (
onDelete: Params,
entityName: string,
user,
) => {
async function callOptionsApi(entityName: string) {
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
try {
const data = await axios(`/${entityName}/autocomplete?limit=100`);
return data.data;
} catch (error) {
console.log(error);
return [];
}
}
const hasUpdatePermission = hasPermission(user, 'UPDATE_CLIENTS');
return [
{
field: 'client_name',
headerName: 'Client name',
flex: 1,
minWidth: 120,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
},
{
field: 'date_registered',
headerName: 'Date registered',
flex: 1,
minWidth: 120,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
type: 'dateTime',
valueGetter: (params: GridValueGetterParams) =>
new Date(params.row.date_registered),
},
{
field: 'clients_manager',
headerName: 'Clients_manager',
flex: 1,
minWidth: 120,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
sortable: false,
type: 'singleSelect',
getOptionValue: (value: any) => value?.id,
getOptionLabel: (value: any) => value?.label,
valueOptions: await callOptionsApi('staff'),
valueGetter: (params: GridValueGetterParams) =>
params?.value?.id ?? params?.value,
},
{
field: 'actions',
type: 'actions',
minWidth: 30,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
getActions: (params: GridRowParams) => {
return [
<ListActionsPopover
onDelete={onDelete}
itemId={params?.row?.id}
pathEdit={`/clients/clients-edit/?id=${params?.row?.id}`}
pathView={`/clients/clients-view/?id=${params?.row?.id}`}
key={1}
hasUpdatePermission={hasUpdatePermission}
/>,
];
},
},
];
};

View File

@ -0,0 +1,109 @@
import React from 'react';
import ImageField from '../ImageField';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import dataFormatter from '../../helpers/dataFormatter';
import { Pagination } from '../Pagination';
import { saveFile } from '../../helpers/fileSaver';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link';
import { hasPermission } from '../../helpers/userPermissions';
type Props = {
staff: any[];
loading: boolean;
onDelete: (id: string) => void;
currentPage: number;
numPages: number;
onPageChange: (page: number) => void;
};
const CardStaff = ({
staff,
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);
const currentUser = useAppSelector((state) => state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_STAFF');
return (
<div className={'p-4'}>
{loading && <LoadingSpinner />}
<ul
role='list'
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
>
{!loading &&
staff.map((item, index) => (
<li
key={item.id}
className={`overflow-hidden ${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} border ${focusRing} border-gray-200 dark:border-dark-700 ${
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
>
<div
className={`flex items-center ${bgColor} p-6 gap-x-4 border-b border-gray-900/5 bg-gray-50 dark:bg-dark-800 relative`}
>
<Link
href={`/staff/staff-view/?id=${item.id}`}
className='text-lg font-bold leading-6 line-clamp-1'
>
{item.employee_name}
</Link>
<div className='ml-auto '>
<ListActionsPopover
onDelete={onDelete}
itemId={item.id}
pathEdit={`/staff/staff-edit/?id=${item.id}`}
pathView={`/staff/staff-view/?id=${item.id}`}
hasUpdatePermission={hasUpdatePermission}
/>
</div>
</div>
<dl className='divide-y divide-stone-300 dark:divide-dark-700 px-6 py-4 text-sm leading-6 h-64 overflow-y-auto'>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>
Employee name
</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{item.employee_name}
</div>
</dd>
</div>
</dl>
</li>
))}
{!loading && staff.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
</div>
)}
</ul>
<div className={'flex items-center justify-center my-6'}>
<Pagination
currentPage={currentPage}
numPages={numPages}
setCurrentPage={onPageChange}
/>
</div>
</div>
);
};
export default CardStaff;

View File

@ -0,0 +1,90 @@
import React from 'react';
import CardBox from '../CardBox';
import ImageField from '../ImageField';
import dataFormatter from '../../helpers/dataFormatter';
import { saveFile } from '../../helpers/fileSaver';
import ListActionsPopover from '../ListActionsPopover';
import { useAppSelector } from '../../stores/hooks';
import { Pagination } from '../Pagination';
import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link';
import { hasPermission } from '../../helpers/userPermissions';
type Props = {
staff: any[];
loading: boolean;
onDelete: (id: string) => void;
currentPage: number;
numPages: number;
onPageChange: (page: number) => void;
};
const ListStaff = ({
staff,
loading,
onDelete,
currentPage,
numPages,
onPageChange,
}: Props) => {
const currentUser = useAppSelector((state) => state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_STAFF');
const corners = useAppSelector((state) => state.style.corners);
const bgColor = useAppSelector((state) => state.style.cardsColor);
return (
<>
<div className='relative overflow-x-auto p-4 space-y-4'>
{loading && <LoadingSpinner />}
{!loading &&
staff.map((item) => (
<CardBox
hasTable
isList
key={item.id}
className={'rounded shadow-none'}
>
<div
className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}
>
<Link
href={`/staff/staff-view/?id=${item.id}`}
className={
'flex-1 px-4 py-6 h-24 flex divide-x-2 divide-stone-300 items-center overflow-hidden`}> dark:divide-dark-700 overflow-x-auto'
}
>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Employee name</p>
<p className={'line-clamp-2'}>{item.employee_name}</p>
</div>
</Link>
<ListActionsPopover
onDelete={onDelete}
itemId={item.id}
pathEdit={`/staff/staff-edit/?id=${item.id}`}
pathView={`/staff/staff-view/?id=${item.id}`}
hasUpdatePermission={hasUpdatePermission}
/>
</div>
</CardBox>
))}
{!loading && staff.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
</div>
)}
</div>
<div className={'flex items-center justify-center my-6'}>
<Pagination
currentPage={currentPage}
numPages={numPages}
setCurrentPage={onPageChange}
/>
</div>
</>
);
};
export default ListStaff;

View File

@ -0,0 +1,481 @@
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/staffSlice';
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 './configureStaffCols';
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter';
import { dataGridStyles } from '../../styles';
const perPage = 10;
const TableSampleStaff = ({
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<GridColDef[]>([]);
const [selectedRows, setSelectedRows] = useState([]);
const [sortModel, setSortModel] = useState([
{
field: '',
sort: 'desc',
},
]);
const {
staff,
loading,
count,
notify: staffNotify,
refetch,
} = useAppSelector((state) => state.staff);
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 (staffNotify.showNotification) {
notify(staffNotify.typeNotification, staffNotify.textNotification);
}
}, [staffNotify.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(() => {
if (!currentUser) return;
loadColumns(handleDeleteModalAction, `staff`, currentUser).then((newCols) =>
setColumns(newCols),
);
}, [currentUser]);
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 rounded dark:placeholder-gray-400 ' +
` ${bgColor} ${focusRing} ${corners} ` +
'dark:bg-slate-800 border';
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
rows={staff ?? []}
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);
}}
/>
</div>
);
return (
<>
{filterItems && Array.isArray(filterItems) && filterItems.length ? (
<CardBox>
<Formik
initialValues={{
checkboxes: ['lorem'],
switches: ['lorem'],
radio: 'lorem',
}}
onSubmit={() => null}
>
<Form>
<>
{filterItems &&
filterItems.map((filterItem) => {
return (
<div key={filterItem.id} className='flex mb-4'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
Filter
</div>
<Field
className={controlClasses}
name='selectedField'
id='selectedField'
component='select'
value={filterItem?.fields?.selectedField || ''}
onChange={handleChange(filterItem.id)}
>
{filters.map((selectOption) => (
<option
key={selectOption.title}
value={`${selectOption.title}`}
>
{selectOption.label}
</option>
))}
</Field>
</div>
{filters.find(
(filter) =>
filter.title === filterItem?.fields?.selectedField,
)?.type === 'enum' ? (
<div className='flex flex-col w-full mr-3'>
<div className='text-gray-500 font-bold'>Value</div>
<Field
className={controlClasses}
name='filterValue'
id='filterValue'
component='select'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value=''>Select Value</option>
{filters
.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField,
)
?.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Field>
</div>
) : filters.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField,
)?.number ? (
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
value={
filterItem?.fields?.filterValueFrom || ''
}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>
To
</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : filters.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField,
)?.date ? (
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
type='datetime-local'
value={
filterItem?.fields?.filterValueFrom || ''
}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>
To
</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : (
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
Contains
</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
)}
<div className='flex flex-col'>
<div className=' text-gray-500 font-bold'>
Action
</div>
<BaseButton
className='my-2'
type='reset'
color='danger'
label='Delete'
onClick={() => {
deleteFilter(filterItem.id);
}}
/>
</div>
</div>
);
})}
<div className='flex'>
<BaseButton
className='my-2 mr-3'
color='success'
label='Apply'
onClick={handleSubmit}
/>
<BaseButton
className='my-2'
color='info'
label='Cancel'
onClick={handleReset}
/>
</div>
</>
</Form>
</Formik>
</CardBox>
) : null}
<CardBoxModal
title='Please confirm'
buttonColor='info'
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
</CardBoxModal>
{dataGrid}
{selectedRows.length > 0 &&
createPortal(
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),
)}
<ToastContainer />
</>
);
};
export default TableSampleStaff;

View File

@ -0,0 +1,73 @@
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 ImageField from '../ImageField';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import DataGridMultiSelect from '../DataGridMultiSelect';
import ListActionsPopover from '../ListActionsPopover';
import { hasPermission } from '../../helpers/userPermissions';
type Params = (id: string) => void;
export const loadColumns = async (
onDelete: Params,
entityName: string,
user,
) => {
async function callOptionsApi(entityName: string) {
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
try {
const data = await axios(`/${entityName}/autocomplete?limit=100`);
return data.data;
} catch (error) {
console.log(error);
return [];
}
}
const hasUpdatePermission = hasPermission(user, 'UPDATE_STAFF');
return [
{
field: 'employee_name',
headerName: 'Employee name',
flex: 1,
minWidth: 120,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
},
{
field: 'actions',
type: 'actions',
minWidth: 30,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
getActions: (params: GridRowParams) => {
return [
<ListActionsPopover
onDelete={onDelete}
itemId={params?.row?.id}
pathEdit={`/staff/staff-edit/?id=${params?.row?.id}`}
pathView={`/staff/staff-view/?id=${params?.row?.id}`}
key={1}
hasUpdatePermission={hasUpdatePermission}
/>,
];
},
},
];
};

View File

@ -18,9 +18,9 @@ export default function WebSiteFooter({
const borders = useAppSelector((state) => state.style.borders);
const websiteHeder = useAppSelector((state) => state.style.websiteHeder);
const style = FooterStyle.WITH_PAGES;
const style = FooterStyle.WITH_PROJECT_NAME;
const design = FooterDesigns.DESIGN_DIVERSITY;
const design = FooterDesigns.DEFAULT_DESIGN;
return (
<div

View File

@ -19,9 +19,9 @@ export default function WebSiteHeader({
const websiteHeder = useAppSelector((state) => state.style.websiteHeder);
const borders = useAppSelector((state) => state.style.borders);
const style = HeaderStyle.PAGES_RIGHT;
const style = HeaderStyle.PAGES_LEFT;
const design = HeaderDesigns.DEFAULT_DESIGN;
const design = HeaderDesigns.DESIGN_DIVERSITY;
return (
<header id='websiteHeader' className='overflow-hidden'>
<div

View File

@ -87,19 +87,6 @@ const CardWork_orders = ({
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>
ProductionManager
</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{dataFormatter.usersOneListFormatter(
item.production_manager,
)}
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>
RawMaterials

View File

@ -60,17 +60,6 @@ const ListWork_orders = ({
<p className={'line-clamp-2'}>{item.order_number}</p>
</div>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>
ProductionManager
</p>
<p className={'line-clamp-2'}>
{dataFormatter.usersOneListFormatter(
item.production_manager,
)}
</p>
</div>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>RawMaterials</p>
<p className={'line-clamp-2'}>

View File

@ -50,26 +50,6 @@ export const loadColumns = async (
editable: hasUpdatePermission,
},
{
field: 'production_manager',
headerName: 'ProductionManager',
flex: 1,
minWidth: 120,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
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: 'raw_materials',
headerName: 'RawMaterials',

View File

@ -39,25 +39,6 @@ export default {
});
},
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 };
},
machineryManyListFormatter(val) {
if (!val || !val.length) return [];
return val.map((item) => item.machine_name);
@ -190,4 +171,23 @@ export default {
if (!val) return '';
return { label: val.name, id: val.id };
},
staffManyListFormatter(val) {
if (!val || !val.length) return [];
return val.map((item) => item.employee_name);
},
staffOneListFormatter(val) {
if (!val) return '';
return val.employee_name;
},
staffManyListFormatterEdit(val) {
if (!val || !val.length) return [];
return val.map((item) => {
return { id: item.id, label: item.employee_name };
});
},
staffOneListFormatterEdit(val) {
if (!val) return '';
return { label: val.employee_name, id: val.id };
},
};

View File

@ -100,6 +100,22 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
permissions: 'READ_COMPANIES',
},
{
href: '/staff/staff-list',
label: 'Staff',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
permissions: 'READ_STAFF',
},
{
href: '/clients/clients-list',
label: 'Clients',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
permissions: 'READ_CLIENTS',
},
{
href: '/profile',
label: 'Profile',

View File

@ -0,0 +1,178 @@
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 FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
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/clients/clientsSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from '../../components/ImageField';
import { hasPermission } from '../../helpers/userPermissions';
const EditClients = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const initVals = {
companies: null,
client_name: '',
date_registered: new Date(),
clients_manager: null,
};
const [initialValues, setInitialValues] = useState(initVals);
const { clients } = useAppSelector((state) => state.clients);
const { currentUser } = useAppSelector((state) => state.auth);
const { clientsId } = router.query;
useEffect(() => {
dispatch(fetch({ id: clientsId }));
}, [clientsId]);
useEffect(() => {
if (typeof clients === 'object') {
setInitialValues(clients);
}
}, [clients]);
useEffect(() => {
if (typeof clients === 'object') {
const newInitialVal = { ...initVals };
Object.keys(initVals).forEach((el) => (newInitialVal[el] = clients[el]));
setInitialValues(newInitialVal);
}
}, [clients]);
const handleSubmit = async (data) => {
await dispatch(update({ id: clientsId, data }));
await router.push('/clients/clients-list');
};
return (
<>
<Head>
<title>{getPageTitle('Edit clients')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={'Edit clients'}
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='companies' labelFor='companies'>
<Field
name='companies'
id='companies'
component={SelectField}
options={initialValues.companies}
itemRef={'companies'}
showField={'name'}
></Field>
</FormField>
<FormField label='Client name'>
<Field name='client_name' placeholder='Client name' />
</FormField>
<FormField label='Date registered'>
<DatePicker
dateFormat='yyyy-MM-dd hh:mm'
showTimeSelect
selected={
initialValues.date_registered
? new Date(
dayjs(initialValues.date_registered).format(
'YYYY-MM-DD hh:mm',
),
)
: null
}
onChange={(date) =>
setInitialValues({
...initialValues,
date_registered: date,
})
}
/>
</FormField>
<FormField label='Clients_manager' labelFor='clients_manager'>
<Field
name='clients_manager'
id='clients_manager'
component={SelectField}
options={initialValues.clients_manager}
itemRef={'staff'}
showField={'employee_name'}
></Field>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/clients/clients-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
EditClients.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'UPDATE_CLIENTS'}>
{page}
</LayoutAuthenticated>
);
};
export default EditClients;

View File

@ -0,0 +1,176 @@
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 FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
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/clients/clientsSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from '../../components/ImageField';
import { hasPermission } from '../../helpers/userPermissions';
const EditClientsPage = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const initVals = {
companies: null,
client_name: '',
date_registered: new Date(),
clients_manager: null,
};
const [initialValues, setInitialValues] = useState(initVals);
const { clients } = useAppSelector((state) => state.clients);
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
useEffect(() => {
dispatch(fetch({ id: id }));
}, [id]);
useEffect(() => {
if (typeof clients === 'object') {
setInitialValues(clients);
}
}, [clients]);
useEffect(() => {
if (typeof clients === 'object') {
const newInitialVal = { ...initVals };
Object.keys(initVals).forEach((el) => (newInitialVal[el] = clients[el]));
setInitialValues(newInitialVal);
}
}, [clients]);
const handleSubmit = async (data) => {
await dispatch(update({ id: id, data }));
await router.push('/clients/clients-list');
};
return (
<>
<Head>
<title>{getPageTitle('Edit clients')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={'Edit clients'}
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='companies' labelFor='companies'>
<Field
name='companies'
id='companies'
component={SelectField}
options={initialValues.companies}
itemRef={'companies'}
showField={'name'}
></Field>
</FormField>
<FormField label='Client name'>
<Field name='client_name' placeholder='Client name' />
</FormField>
<FormField label='Date registered'>
<DatePicker
dateFormat='yyyy-MM-dd hh:mm'
showTimeSelect
selected={
initialValues.date_registered
? new Date(
dayjs(initialValues.date_registered).format(
'YYYY-MM-DD hh:mm',
),
)
: null
}
onChange={(date) =>
setInitialValues({
...initialValues,
date_registered: date,
})
}
/>
</FormField>
<FormField label='Clients_manager' labelFor='clients_manager'>
<Field
name='clients_manager'
id='clients_manager'
component={SelectField}
options={initialValues.clients_manager}
itemRef={'staff'}
showField={'employee_name'}
></Field>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/clients/clients-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
EditClientsPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'UPDATE_CLIENTS'}>
{page}
</LayoutAuthenticated>
);
};
export default EditClientsPage;

View File

@ -0,0 +1,168 @@
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 TableClients from '../../components/Clients/TableClients';
import BaseButton from '../../components/BaseButton';
import axios from 'axios';
import Link from 'next/link';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import CardBoxModal from '../../components/CardBoxModal';
import DragDropFilePicker from '../../components/DragDropFilePicker';
import { setRefetch, uploadCsv } from '../../stores/clients/clientsSlice';
import { hasPermission } from '../../helpers/userPermissions';
const ClientsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([
{ label: 'Client name', title: 'client_name' },
{ label: 'Date registered', title: 'date_registered', date: 'true' },
{ label: 'Clients_manager', title: 'clients_manager' },
]);
const hasCreatePermission =
currentUser && hasPermission(currentUser, 'CREATE_CLIENTS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getClientsCSV = async () => {
const response = await axios({
url: '/clients?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 = 'clientsCSV.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 (
<>
<Head>
<title>{getPageTitle('Clients')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Clients'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && (
<BaseButton
className={'mr-3'}
href={'/clients/clients-new'}
color='info'
label='New Item'
/>
)}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton
className={'mr-3'}
color='info'
label='Download CSV'
onClick={getClientsCSV}
/>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className='mb-6' hasTable>
<TableClients
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</>
);
};
ClientsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_CLIENTS'}>
{page}
</LayoutAuthenticated>
);
};
export default ClientsTablesPage;

View File

@ -0,0 +1,132 @@
import {
mdiAccount,
mdiChartTimelineVariant,
mdiMail,
mdiUpload,
} 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 FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
import { SwitchField } from '../../components/SwitchField';
import { SelectField } from '../../components/SelectField';
import { SelectFieldMany } from '../../components/SelectFieldMany';
import { RichTextField } from '../../components/RichTextField';
import { create } from '../../stores/clients/clientsSlice';
import { useAppDispatch } from '../../stores/hooks';
import { useRouter } from 'next/router';
import moment from 'moment';
const initialValues = {
companies: '',
client_name: '',
date_registered: '',
clients_manager: '',
};
const ClientsNew = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const handleSubmit = async (data) => {
await dispatch(create(data));
await router.push('/clients/clients-list');
};
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='New Item'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='companies' labelFor='companies'>
<Field
name='companies'
id='companies'
component={SelectField}
options={[]}
itemRef={'companies'}
></Field>
</FormField>
<FormField label='Client name'>
<Field name='client_name' placeholder='Client name' />
</FormField>
<FormField label='Date registered'>
<Field
type='datetime-local'
name='date_registered'
placeholder='Date registered'
/>
</FormField>
<FormField label='Clients_manager' labelFor='clients_manager'>
<Field
name='clients_manager'
id='clients_manager'
component={SelectField}
options={[]}
itemRef={'staff'}
></Field>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/clients/clients-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
ClientsNew.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'CREATE_CLIENTS'}>
{page}
</LayoutAuthenticated>
);
};
export default ClientsNew;

View File

@ -0,0 +1,167 @@
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 TableClients from '../../components/Clients/TableClients';
import BaseButton from '../../components/BaseButton';
import axios from 'axios';
import Link from 'next/link';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import CardBoxModal from '../../components/CardBoxModal';
import DragDropFilePicker from '../../components/DragDropFilePicker';
import { setRefetch, uploadCsv } from '../../stores/clients/clientsSlice';
import { hasPermission } from '../../helpers/userPermissions';
const ClientsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([
{ label: 'Client name', title: 'client_name' },
{ label: 'Date registered', title: 'date_registered', date: 'true' },
{ label: 'Clients_manager', title: 'clients_manager' },
]);
const hasCreatePermission =
currentUser && hasPermission(currentUser, 'CREATE_CLIENTS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getClientsCSV = async () => {
const response = await axios({
url: '/clients?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 = 'clientsCSV.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 (
<>
<Head>
<title>{getPageTitle('Clients')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Clients'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && (
<BaseButton
className={'mr-3'}
href={'/clients/clients-new'}
color='info'
label='New Item'
/>
)}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton
className={'mr-3'}
color='info'
label='Download CSV'
onClick={getClientsCSV}
/>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className='mb-6' hasTable>
<TableClients
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={true}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</>
);
};
ClientsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_CLIENTS'}>
{page}
</LayoutAuthenticated>
);
};
export default ClientsTablesPage;

View File

@ -0,0 +1,120 @@
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/clients/clientsSlice';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from '../../components/ImageField';
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';
import { hasPermission } from '../../helpers/userPermissions';
const ClientsView = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const { clients } = useAppSelector((state) => state.clients);
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str, `str`);
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View clients')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={removeLastCharacter('View clients')}
main
>
<BaseButton
color='info'
label='Edit'
href={`/clients/clients-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
<CardBox>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>companies</p>
<p>{clients?.companies?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Client name</p>
<p>{clients?.client_name}</p>
</div>
<FormField label='Date registered'>
{clients.date_registered ? (
<DatePicker
dateFormat='yyyy-MM-dd hh:mm'
showTimeSelect
selected={
clients.date_registered
? new Date(
dayjs(clients.date_registered).format(
'YYYY-MM-DD hh:mm',
),
)
: null
}
disabled
/>
) : (
<p>No Date registered</p>
)}
</FormField>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Clients_manager</p>
<p>{clients?.clients_manager?.employee_name ?? 'No data'}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/clients/clients-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
ClientsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_CLIENTS'}>
{page}
</LayoutAuthenticated>
);
};
export default ClientsView;

View File

@ -459,6 +459,86 @@ const CompaniesView = () => {
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Staff companies</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Employee name</th>
</tr>
</thead>
<tbody>
{companies.staff_companies &&
Array.isArray(companies.staff_companies) &&
companies.staff_companies.map((item: any) => (
<tr
key={item.id}
onClick={() =>
router.push(`/staff/staff-view/?id=${item.id}`)
}
>
<td data-label='employee_name'>
{item.employee_name}
</td>
</tr>
))}
</tbody>
</table>
</div>
{!companies?.staff_companies?.length && (
<div className={'text-center py-4'}>No data</div>
)}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Clients companies</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Client name</th>
<th>Date registered</th>
</tr>
</thead>
<tbody>
{companies.clients_companies &&
Array.isArray(companies.clients_companies) &&
companies.clients_companies.map((item: any) => (
<tr
key={item.id}
onClick={() =>
router.push(`/clients/clients-view/?id=${item.id}`)
}
>
<td data-label='client_name'>{item.client_name}</td>
<td data-label='date_registered'>
{dataFormatter.dateTimeFormatter(
item.date_registered,
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{!companies?.clients_companies?.length && (
<div className={'text-center py-4'}>No data</div>
)}
</CardBox>
</>
<BaseDivider />
<BaseButton

View File

@ -33,6 +33,8 @@ const Dashboard = () => {
const [roles, setRoles] = React.useState('Loading...');
const [permissions, setPermissions] = React.useState('Loading...');
const [companies, setCompanies] = React.useState('Loading...');
const [staff, setStaff] = React.useState('Loading...');
const [clients, setClients] = React.useState('Loading...');
const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
@ -57,6 +59,8 @@ const Dashboard = () => {
'roles',
'permissions',
'companies',
'staff',
'clients',
];
const fns = [
setUsers,
@ -70,6 +74,8 @@ const Dashboard = () => {
setRoles,
setPermissions,
setCompanies,
setStaff,
setClients,
];
const requests = entities.map((entity, index) => {
@ -529,6 +535,70 @@ const Dashboard = () => {
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_STAFF') && (
<Link href={'/staff/staff-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Staff
</div>
<div className='text-3xl leading-tight font-semibold'>
{staff}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_CLIENTS') && (
<Link href={'/clients/clients-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Clients
</div>
<div className='text-3xl leading-tight font-semibold'>
{clients}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
</div>
</SectionMain>
</>

View File

@ -139,7 +139,7 @@ export default function WebSite() {
<FeaturesSection
projectName={'Test Editor'}
image={['Dashboard showing ERP metrics']}
withBg={0}
withBg={1}
features={features_points}
mainText={`Unlock Efficiency with ${projectName} Features`}
subTitle={`Explore the powerful features of ${projectName} designed to streamline your manufacturing operations and boost productivity.`}

View File

@ -0,0 +1,141 @@
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 FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
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/staff/staffSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from '../../components/ImageField';
import { hasPermission } from '../../helpers/userPermissions';
const EditStaff = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const initVals = {
companies: null,
employee_name: '',
};
const [initialValues, setInitialValues] = useState(initVals);
const { staff } = useAppSelector((state) => state.staff);
const { currentUser } = useAppSelector((state) => state.auth);
const { staffId } = router.query;
useEffect(() => {
dispatch(fetch({ id: staffId }));
}, [staffId]);
useEffect(() => {
if (typeof staff === 'object') {
setInitialValues(staff);
}
}, [staff]);
useEffect(() => {
if (typeof staff === 'object') {
const newInitialVal = { ...initVals };
Object.keys(initVals).forEach((el) => (newInitialVal[el] = staff[el]));
setInitialValues(newInitialVal);
}
}, [staff]);
const handleSubmit = async (data) => {
await dispatch(update({ id: staffId, data }));
await router.push('/staff/staff-list');
};
return (
<>
<Head>
<title>{getPageTitle('Edit staff')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={'Edit staff'}
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='companies' labelFor='companies'>
<Field
name='companies'
id='companies'
component={SelectField}
options={initialValues.companies}
itemRef={'companies'}
showField={'name'}
></Field>
</FormField>
<FormField label='Employee name'>
<Field name='employee_name' placeholder='Employee name' />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/staff/staff-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
EditStaff.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'UPDATE_STAFF'}>
{page}
</LayoutAuthenticated>
);
};
export default EditStaff;

View File

@ -0,0 +1,139 @@
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 FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
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/staff/staffSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from '../../components/ImageField';
import { hasPermission } from '../../helpers/userPermissions';
const EditStaffPage = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const initVals = {
companies: null,
employee_name: '',
};
const [initialValues, setInitialValues] = useState(initVals);
const { staff } = useAppSelector((state) => state.staff);
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
useEffect(() => {
dispatch(fetch({ id: id }));
}, [id]);
useEffect(() => {
if (typeof staff === 'object') {
setInitialValues(staff);
}
}, [staff]);
useEffect(() => {
if (typeof staff === 'object') {
const newInitialVal = { ...initVals };
Object.keys(initVals).forEach((el) => (newInitialVal[el] = staff[el]));
setInitialValues(newInitialVal);
}
}, [staff]);
const handleSubmit = async (data) => {
await dispatch(update({ id: id, data }));
await router.push('/staff/staff-list');
};
return (
<>
<Head>
<title>{getPageTitle('Edit staff')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={'Edit staff'}
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='companies' labelFor='companies'>
<Field
name='companies'
id='companies'
component={SelectField}
options={initialValues.companies}
itemRef={'companies'}
showField={'name'}
></Field>
</FormField>
<FormField label='Employee name'>
<Field name='employee_name' placeholder='Employee name' />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/staff/staff-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
EditStaffPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'UPDATE_STAFF'}>
{page}
</LayoutAuthenticated>
);
};
export default EditStaffPage;

View File

@ -0,0 +1,162 @@
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 TableStaff from '../../components/Staff/TableStaff';
import BaseButton from '../../components/BaseButton';
import axios from 'axios';
import Link from 'next/link';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import CardBoxModal from '../../components/CardBoxModal';
import DragDropFilePicker from '../../components/DragDropFilePicker';
import { setRefetch, uploadCsv } from '../../stores/staff/staffSlice';
import { hasPermission } from '../../helpers/userPermissions';
const StaffTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([
{ label: 'Employee name', title: 'employee_name' },
]);
const hasCreatePermission =
currentUser && hasPermission(currentUser, 'CREATE_STAFF');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getStaffCSV = async () => {
const response = await axios({
url: '/staff?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 = 'staffCSV.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 (
<>
<Head>
<title>{getPageTitle('Staff')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Staff'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && (
<BaseButton
className={'mr-3'}
href={'/staff/staff-new'}
color='info'
label='New Item'
/>
)}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton
className={'mr-3'}
color='info'
label='Download CSV'
onClick={getStaffCSV}
/>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className='mb-6' hasTable>
<TableStaff
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</>
);
};
StaffTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_STAFF'}>{page}</LayoutAuthenticated>
);
};
export default StaffTablesPage;

View File

@ -0,0 +1,110 @@
import {
mdiAccount,
mdiChartTimelineVariant,
mdiMail,
mdiUpload,
} 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 FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
import { SwitchField } from '../../components/SwitchField';
import { SelectField } from '../../components/SelectField';
import { SelectFieldMany } from '../../components/SelectFieldMany';
import { RichTextField } from '../../components/RichTextField';
import { create } from '../../stores/staff/staffSlice';
import { useAppDispatch } from '../../stores/hooks';
import { useRouter } from 'next/router';
import moment from 'moment';
const initialValues = {
companies: '',
employee_name: '',
};
const StaffNew = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const handleSubmit = async (data) => {
await dispatch(create(data));
await router.push('/staff/staff-list');
};
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='New Item'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='companies' labelFor='companies'>
<Field
name='companies'
id='companies'
component={SelectField}
options={[]}
itemRef={'companies'}
></Field>
</FormField>
<FormField label='Employee name'>
<Field name='employee_name' placeholder='Employee name' />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton
type='reset'
color='danger'
outline
label='Cancel'
onClick={() => router.push('/staff/staff-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
StaffNew.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'CREATE_STAFF'}>
{page}
</LayoutAuthenticated>
);
};
export default StaffNew;

View File

@ -0,0 +1,161 @@
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 TableStaff from '../../components/Staff/TableStaff';
import BaseButton from '../../components/BaseButton';
import axios from 'axios';
import Link from 'next/link';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import CardBoxModal from '../../components/CardBoxModal';
import DragDropFilePicker from '../../components/DragDropFilePicker';
import { setRefetch, uploadCsv } from '../../stores/staff/staffSlice';
import { hasPermission } from '../../helpers/userPermissions';
const StaffTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([
{ label: 'Employee name', title: 'employee_name' },
]);
const hasCreatePermission =
currentUser && hasPermission(currentUser, 'CREATE_STAFF');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getStaffCSV = async () => {
const response = await axios({
url: '/staff?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 = 'staffCSV.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 (
<>
<Head>
<title>{getPageTitle('Staff')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Staff'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && (
<BaseButton
className={'mr-3'}
href={'/staff/staff-new'}
color='info'
label='New Item'
/>
)}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton
className={'mr-3'}
color='info'
label='Download CSV'
onClick={getStaffCSV}
/>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className='mb-6' hasTable>
<TableStaff
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={true}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</>
);
};
StaffTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_STAFF'}>{page}</LayoutAuthenticated>
);
};
export default StaffTablesPage;

View File

@ -0,0 +1,134 @@
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/staff/staffSlice';
import { saveFile } from '../../helpers/fileSaver';
import dataFormatter from '../../helpers/dataFormatter';
import ImageField from '../../components/ImageField';
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';
import { hasPermission } from '../../helpers/userPermissions';
const StaffView = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const { staff } = useAppSelector((state) => state.staff);
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str, `str`);
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
return (
<>
<Head>
<title>{getPageTitle('View staff')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={removeLastCharacter('View staff')}
main
>
<BaseButton
color='info'
label='Edit'
href={`/staff/staff-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
<CardBox>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>companies</p>
<p>{staff?.companies?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Employee name</p>
<p>{staff?.employee_name}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Clients Clients_manager</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Client name</th>
<th>Date registered</th>
</tr>
</thead>
<tbody>
{staff.clients_clients_manager &&
Array.isArray(staff.clients_clients_manager) &&
staff.clients_clients_manager.map((item: any) => (
<tr
key={item.id}
onClick={() =>
router.push(`/clients/clients-view/?id=${item.id}`)
}
>
<td data-label='client_name'>{item.client_name}</td>
<td data-label='date_registered'>
{dataFormatter.dateTimeFormatter(
item.date_registered,
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{!staff?.clients_clients_manager?.length && (
<div className={'text-center py-4'}>No data</div>
)}
</CardBox>
</>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/staff/staff-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
StaffView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_STAFF'}>{page}</LayoutAuthenticated>
);
};
export default StaffView;

View File

@ -148,57 +148,6 @@ const UsersView = () => {
<p>{users?.companies?.name ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>
Work_orders ProductionManager
</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>OrderNumber</th>
<th>StartDate</th>
<th>EndDate</th>
</tr>
</thead>
<tbody>
{users.work_orders_production_manager &&
Array.isArray(users.work_orders_production_manager) &&
users.work_orders_production_manager.map((item: any) => (
<tr
key={item.id}
onClick={() =>
router.push(
`/work_orders/work_orders-view/?id=${item.id}`,
)
}
>
<td data-label='order_number'>{item.order_number}</td>
<td data-label='start_date'>
{dataFormatter.dateTimeFormatter(item.start_date)}
</td>
<td data-label='end_date'>
{dataFormatter.dateTimeFormatter(item.end_date)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{!users?.work_orders_production_manager?.length && (
<div className={'text-center py-4'}>No data</div>
)}
</CardBox>
</>
<BaseDivider />
<BaseButton

View File

@ -145,7 +145,7 @@ export default function WebSite() {
<FeaturesSection
projectName={'Test Editor'}
image={['ERP dashboard on a screen']}
withBg={0}
withBg={1}
features={features_points}
mainText={`Explore ${projectName} Core Features`}
subTitle={`Discover the powerful features of ${projectName} that streamline your manufacturing operations and drive success.`}

View File

@ -139,7 +139,7 @@ export default function WebSite() {
<FeaturesSection
projectName={'Test Editor'}
image={['Dashboard showing ERP metrics']}
withBg={1}
withBg={0}
features={features_points}
mainText={`Unlock Efficiency with ${projectName} Features`}
subTitle={`Explore the powerful features of ${projectName} designed to streamline your manufacturing operations and boost productivity.`}

View File

@ -40,8 +40,6 @@ const EditWork_orders = () => {
const initVals = {
order_number: '',
production_manager: null,
raw_materials: [],
machinery: [],
@ -111,20 +109,6 @@ const EditWork_orders = () => {
<Field name='order_number' placeholder='OrderNumber' />
</FormField>
<FormField
label='ProductionManager'
labelFor='production_manager'
>
<Field
name='production_manager'
id='production_manager'
component={SelectField}
options={initialValues.production_manager}
itemRef={'users'}
showField={'firstName'}
></Field>
</FormField>
<FormField label='RawMaterials' labelFor='raw_materials'>
<Field
name='raw_materials'

View File

@ -40,8 +40,6 @@ const EditWork_ordersPage = () => {
const initVals = {
order_number: '',
production_manager: null,
raw_materials: [],
machinery: [],
@ -109,20 +107,6 @@ const EditWork_ordersPage = () => {
<Field name='order_number' placeholder='OrderNumber' />
</FormField>
<FormField
label='ProductionManager'
labelFor='production_manager'
>
<Field
name='production_manager'
id='production_manager'
component={SelectField}
options={initialValues.production_manager}
itemRef={'users'}
showField={'firstName'}
></Field>
</FormField>
<FormField label='RawMaterials' labelFor='raw_materials'>
<Field
name='raw_materials'

View File

@ -37,8 +37,6 @@ const Work_ordersTablesPage = () => {
{ label: 'StartDate', title: 'start_date', date: 'true' },
{ label: 'EndDate', title: 'end_date', date: 'true' },
{ label: 'ProductionManager', title: 'production_manager' },
{ label: 'RawMaterials', title: 'raw_materials' },
{ label: 'Machinery', title: 'machinery' },
]);

View File

@ -35,8 +35,6 @@ import moment from 'moment';
const initialValues = {
order_number: '',
production_manager: '',
raw_materials: [],
machinery: [],
@ -91,19 +89,6 @@ const Work_ordersNew = () => {
<Field name='order_number' placeholder='OrderNumber' />
</FormField>
<FormField
label='ProductionManager'
labelFor='production_manager'
>
<Field
name='production_manager'
id='production_manager'
component={SelectField}
options={[]}
itemRef={'users'}
></Field>
</FormField>
<FormField label='RawMaterials' labelFor='raw_materials'>
<Field
name='raw_materials'

View File

@ -37,8 +37,6 @@ const Work_ordersTablesPage = () => {
{ label: 'StartDate', title: 'start_date', date: 'true' },
{ label: 'EndDate', title: 'end_date', date: 'true' },
{ label: 'ProductionManager', title: 'production_manager' },
{ label: 'RawMaterials', title: 'raw_materials' },
{ label: 'Machinery', title: 'machinery' },
]);

View File

@ -63,12 +63,6 @@ const Work_ordersView = () => {
<p>{work_orders?.order_number}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ProductionManager</p>
<p>{work_orders?.production_manager?.firstName ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>RawMaterials</p>
<CardBox

View File

@ -0,0 +1,236 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import {
fulfilledNotify,
rejectNotify,
resetNotify,
} from '../../helpers/notifyStateHandler';
interface MainState {
clients: any;
loading: boolean;
count: number;
refetch: boolean;
rolesWidgets: any[];
notify: {
showNotification: boolean;
textNotification: string;
typeNotification: string;
};
}
const initialState: MainState = {
clients: [],
loading: false,
count: 0,
refetch: false,
rolesWidgets: [],
notify: {
showNotification: false,
textNotification: '',
typeNotification: 'warn',
},
};
export const fetch = createAsyncThunk('clients/fetch', async (data: any) => {
const { id, query } = data;
const result = await axios.get(`clients${query || (id ? `/${id}` : '')}`);
return id
? result.data
: { rows: result.data.rows, count: result.data.count };
});
export const deleteItemsByIds = createAsyncThunk(
'clients/deleteByIds',
async (data: any, { rejectWithValue }) => {
try {
await axios.post('clients/deleteByIds', { data });
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const deleteItem = createAsyncThunk(
'clients/deleteClients',
async (id: string, { rejectWithValue }) => {
try {
await axios.delete(`clients/${id}`);
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const create = createAsyncThunk(
'clients/createClients',
async (data: any, { rejectWithValue }) => {
try {
const result = await axios.post('clients', { data });
return result.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const uploadCsv = createAsyncThunk(
'clients/uploadCsv',
async (file: File, { rejectWithValue }) => {
try {
const data = new FormData();
data.append('file', file);
data.append('filename', file.name);
const result = await axios.post('clients/bulk-import', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return result.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const update = createAsyncThunk(
'clients/updateClients',
async (payload: any, { rejectWithValue }) => {
try {
const result = await axios.put(`clients/${payload.id}`, {
id: payload.id,
data: payload.data,
});
return result.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const clientsSlice = createSlice({
name: 'clients',
initialState,
reducers: {
setRefetch: (state, action: PayloadAction<boolean>) => {
state.refetch = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetch.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(fetch.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(fetch.fulfilled, (state, action) => {
if (action.payload.rows && action.payload.count >= 0) {
state.clients = action.payload.rows;
state.count = action.payload.count;
} else {
state.clients = action.payload;
}
state.loading = false;
});
builder.addCase(deleteItemsByIds.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(deleteItemsByIds.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, 'Clients has been deleted');
});
builder.addCase(deleteItemsByIds.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(deleteItem.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(deleteItem.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, `${'Clients'.slice(0, -1)} has been deleted`);
});
builder.addCase(deleteItem.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(create.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(create.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(create.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, `${'Clients'.slice(0, -1)} has been created`);
});
builder.addCase(update.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(update.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, `${'Clients'.slice(0, -1)} has been updated`);
});
builder.addCase(update.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(uploadCsv.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(uploadCsv.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, 'Clients has been uploaded');
});
builder.addCase(uploadCsv.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
},
});
// Action creators are generated for each case reducer function
export const { setRefetch } = clientsSlice.actions;
export default clientsSlice.reducer;

View File

@ -0,0 +1,236 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import {
fulfilledNotify,
rejectNotify,
resetNotify,
} from '../../helpers/notifyStateHandler';
interface MainState {
staff: any;
loading: boolean;
count: number;
refetch: boolean;
rolesWidgets: any[];
notify: {
showNotification: boolean;
textNotification: string;
typeNotification: string;
};
}
const initialState: MainState = {
staff: [],
loading: false,
count: 0,
refetch: false,
rolesWidgets: [],
notify: {
showNotification: false,
textNotification: '',
typeNotification: 'warn',
},
};
export const fetch = createAsyncThunk('staff/fetch', async (data: any) => {
const { id, query } = data;
const result = await axios.get(`staff${query || (id ? `/${id}` : '')}`);
return id
? result.data
: { rows: result.data.rows, count: result.data.count };
});
export const deleteItemsByIds = createAsyncThunk(
'staff/deleteByIds',
async (data: any, { rejectWithValue }) => {
try {
await axios.post('staff/deleteByIds', { data });
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const deleteItem = createAsyncThunk(
'staff/deleteStaff',
async (id: string, { rejectWithValue }) => {
try {
await axios.delete(`staff/${id}`);
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const create = createAsyncThunk(
'staff/createStaff',
async (data: any, { rejectWithValue }) => {
try {
const result = await axios.post('staff', { data });
return result.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const uploadCsv = createAsyncThunk(
'staff/uploadCsv',
async (file: File, { rejectWithValue }) => {
try {
const data = new FormData();
data.append('file', file);
data.append('filename', file.name);
const result = await axios.post('staff/bulk-import', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return result.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const update = createAsyncThunk(
'staff/updateStaff',
async (payload: any, { rejectWithValue }) => {
try {
const result = await axios.put(`staff/${payload.id}`, {
id: payload.id,
data: payload.data,
});
return result.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const staffSlice = createSlice({
name: 'staff',
initialState,
reducers: {
setRefetch: (state, action: PayloadAction<boolean>) => {
state.refetch = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetch.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(fetch.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(fetch.fulfilled, (state, action) => {
if (action.payload.rows && action.payload.count >= 0) {
state.staff = action.payload.rows;
state.count = action.payload.count;
} else {
state.staff = action.payload;
}
state.loading = false;
});
builder.addCase(deleteItemsByIds.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(deleteItemsByIds.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, 'Staff has been deleted');
});
builder.addCase(deleteItemsByIds.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(deleteItem.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(deleteItem.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, `${'Staff'.slice(0, -1)} has been deleted`);
});
builder.addCase(deleteItem.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(create.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(create.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(create.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, `${'Staff'.slice(0, -1)} has been created`);
});
builder.addCase(update.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(update.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, `${'Staff'.slice(0, -1)} has been updated`);
});
builder.addCase(update.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
builder.addCase(uploadCsv.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(uploadCsv.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, 'Staff has been uploaded');
});
builder.addCase(uploadCsv.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
},
});
// Action creators are generated for each case reducer function
export const { setRefetch } = staffSlice.actions;
export default staffSlice.reducer;

View File

@ -15,6 +15,8 @@ import work_ordersSlice from './work_orders/work_ordersSlice';
import rolesSlice from './roles/rolesSlice';
import permissionsSlice from './permissions/permissionsSlice';
import companiesSlice from './companies/companiesSlice';
import staffSlice from './staff/staffSlice';
import clientsSlice from './clients/clientsSlice';
export const store = configureStore({
reducer: {
@ -34,6 +36,8 @@ export const store = configureStore({
roles: rolesSlice,
permissions: permissionsSlice,
companies: companiesSlice,
staff: staffSlice,
clients: clientsSlice,
},
});