Compare commits

...

4 Commits

Author SHA1 Message Date
Flatlogic Bot
195e1589a3 Autosave: 20260401-210827 2026-04-01 21:08:28 +00:00
Flatlogic Bot
0704729e11 Autosave: 20260401-194421 2026-04-01 19:44:21 +00:00
Flatlogic Bot
6958be49cc Autosave: 20260331-201135 2026-03-31 20:11:35 +00:00
Flatlogic Bot
64d433d6e7 amr7aj 2026-03-31 19:41:37 +00:00
136 changed files with 3594 additions and 2280 deletions

View File

@ -39,7 +39,7 @@ const config = {
},
uploadDir: os.tmpdir(),
email: {
from: 'Multi-Client Detergents POS <app@flatlogic.app>',
from: 'نظام إدارة المحل ونقطة البيع <app@flatlogic.app>',
host: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
auth: {

View File

@ -1,9 +1,3 @@
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 sales_invoices = sequelize.define(
'sales_invoices',
@ -129,22 +123,20 @@ notes: {
sales_invoices.associate = (db) => {
db.sales_invoices.belongsToMany(db.sales_invoice_items, {
db.sales_invoices.hasMany(db.sales_invoice_items, {
as: 'items',
foreignKey: {
name: 'sales_invoices_itemsId',
name: 'invoiceId',
},
constraints: false,
through: 'sales_invoicesItemsSales_invoice_items',
});
db.sales_invoices.belongsToMany(db.sales_invoice_items, {
db.sales_invoices.hasMany(db.sales_invoice_items, {
as: 'items_filter',
foreignKey: {
name: 'sales_invoices_itemsId',
name: 'invoiceId',
},
constraints: false,
through: 'sales_invoicesItemsSales_invoice_items',
});

View File

@ -42,6 +42,7 @@ const sales_invoicesRoutes = require('./routes/sales_invoices');
const sales_invoice_itemsRoutes = require('./routes/sales_invoice_items');
const price_change_logsRoutes = require('./routes/price_change_logs');
const posRoutes = require('./routes/pos');
const getBaseUrl = (url) => {
@ -54,8 +55,8 @@ const options = {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "Multi-Client Detergents POS",
description: "Multi-Client Detergents POS Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.",
title: "نظام إدارة المحل ونقطة البيع",
description: "واجهة API خاصة بإدارة المحل ونقطة البيع، وتوفّر عمليات الإدارة الأساسية على الكيانات والبيانات.",
},
servers: [
{
@ -120,6 +121,7 @@ app.use('/api/sales_invoices', passport.authenticate('jwt', {session: false}), s
app.use('/api/sales_invoice_items', passport.authenticate('jwt', {session: false}), sales_invoice_itemsRoutes);
app.use('/api/price_change_logs', passport.authenticate('jwt', {session: false}), price_change_logsRoutes);
app.use('/api/pos', passport.authenticate('jwt', {session: false}), posRoutes);
app.use(
'/api/openai',

View File

@ -7,30 +7,23 @@ let publicRoleCache = null;
// Function to asynchronously fetch and cache the 'Public' role
async function fetchAndCachePublicRole() {
try {
// Use RolesDBApi to find the role by name 'Public'
publicRoleCache = await RolesDBApi.findBy({ name: 'Public' });
try {
// Use RolesDBApi to find the role by name 'Public'
publicRoleCache = await RolesDBApi.findBy({ name: 'Public' });
if (!publicRoleCache) {
console.error("WARNING: Role 'Public' not found in database during middleware startup. Check your migrations.");
// The system might not function correctly without this role. May need to throw an error or use a fallback stub.
} else {
console.log("'Public' role successfully loaded and cached.");
}
} catch (error) {
console.error("Error fetching 'Public' role during middleware startup:", error);
// Handle the error during startup fetch
throw error; // Important to know if the app can proceed without the Public role
if (!publicRoleCache) {
console.error("WARNING: لم يتم العثور على دور 'Public' في قاعدة البيانات أثناء تهيئة الصلاحيات.");
} else {
console.log("تم تحميل دور 'Public' وحفظه في الذاكرة المؤقتة بنجاح.");
}
} catch (error) {
console.error("حدث خطأ أثناء تحميل دور 'Public' عند بدء تشغيل وسيط الصلاحيات:", error);
throw error;
}
}
// Trigger the role fetching when the check-permissions.js module is imported/loaded
// This should happen during application startup when routes are being configured.
fetchAndCachePublicRole().catch(error => {
// Handle the case where the fetchAndCachePublicRole promise is rejected
console.error("Critical error during permissions middleware initialization:", error);
// Decide here if the process should exit if the Public role is essential.
// process.exit(1);
fetchAndCachePublicRole().catch((error) => {
console.error('خطأ حرج أثناء تهيئة وسيط الصلاحيات:', error);
});
/**
@ -39,85 +32,63 @@ fetchAndCachePublicRole().catch(error => {
* @return {import("express").RequestHandler} Express middleware function.
*/
function checkPermissions(permission) {
return async (req, res, next) => {
const { currentUser } = req;
return async (req, res, next) => {
const { currentUser } = req;
// 1. Check self-access bypass (only if the user is authenticated)
if (currentUser && (currentUser.id === req.params.id || currentUser.id === req.body.id)) {
return next(); // User has access to their own resource
if (currentUser && (currentUser.id === req.params.id || currentUser.id === req.body.id)) {
return next();
}
if (currentUser) {
const customPermissions = Array.isArray(currentUser.custom_permissions)
? currentUser.custom_permissions
: [];
const userPermission = customPermissions.find(
(cp) => cp.name === permission,
);
if (userPermission) {
return next();
}
}
let effectiveRole = null;
try {
if (currentUser && currentUser.app_role) {
effectiveRole = currentUser.app_role;
} else if (!publicRoleCache) {
console.error("ذاكرة دور 'Public' فارغة. سيتم إعادة تحميله مباشرةً من قاعدة البيانات.");
effectiveRole = await RolesDBApi.findBy({ name: 'Public' });
if (!effectiveRole) {
return next(new Error('خطأ داخلي: تعذر تحميل دور الوصول العام.'));
}
} else {
effectiveRole = publicRoleCache;
}
// 2. Check Custom Permissions (only if the user is authenticated)
if (currentUser) {
// Ensure custom_permissions is an array before using find
const customPermissions = Array.isArray(currentUser.custom_permissions)
? currentUser.custom_permissions
: [];
const userPermission = customPermissions.find(
(cp) => cp.name === permission,
);
if (userPermission) {
return next(); // User has a custom permission
}
}
if (!effectiveRole) {
return next(new Error('خطأ داخلي: تعذر تحديد الدور الفعّال للتحقق من الصلاحية.'));
}
// 3. Determine the "effective" role for permission check
let effectiveRole = null;
try {
if (currentUser && currentUser.app_role) {
// User is authenticated and has an assigned role
effectiveRole = currentUser.app_role;
} else {
// User is NOT authenticated OR is authenticated but has no role
// Use the cached 'Public' role
if (!publicRoleCache) {
// If the cache is unexpectedly empty (e.g., startup error caught),
// we can try fetching the role again synchronously (less ideal) or just deny access.
console.error("Public role cache is empty. Attempting synchronous fetch...");
// Less efficient fallback option:
effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); // Could be slow
if (!effectiveRole) {
// If even the synchronous attempt failed
return next(new Error("Internal Server Error: Public role missing and cannot be fetched."));
}
} else {
effectiveRole = publicRoleCache; // Use the cached object
}
}
let rolePermissions = [];
if (typeof effectiveRole.getPermissions === 'function') {
rolePermissions = await effectiveRole.getPermissions();
} else if (Array.isArray(effectiveRole.permissions)) {
rolePermissions = effectiveRole.permissions;
} else {
console.error('تنسيق كائن الدور غير صالح ولا يحتوي على الصلاحيات المطلوبة:', effectiveRole);
return next(new Error('خطأ داخلي: بيانات الدور غير صالحة للتحقق من الصلاحيات.'));
}
// Check if we got a valid role object
if (!effectiveRole) {
return next(new Error("Internal Server Error: Could not determine effective role."));
}
// 4. Check Permissions on the "effective" role
// Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method
// or a 'permissions' property (if permissions are eagerly loaded).
let rolePermissions = [];
if (typeof effectiveRole.getPermissions === 'function') {
rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists
} else if (Array.isArray(effectiveRole.permissions)) {
rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded
} else {
console.error("Role object lacks getPermissions() method or permissions property:", effectiveRole);
return next(new Error("Internal Server Error: Invalid role object format."));
}
if (rolePermissions.find((p) => p.name === permission)) {
next(); // The "effective" role has the required permission
} else {
// The "effective" role does not have the required permission
const roleName = effectiveRole.name || 'unknown role';
next(new ValidationError('auth.forbidden', `Role '${roleName}' denied access to '${permission}'.`));
}
} catch (e) {
// Handle errors during role or permission fetching
console.error("Error during permission check:", e);
next(e); // Pass the error to the next middleware
}
};
if (rolePermissions.find((p) => p.name === permission)) {
next();
} else {
next(new ValidationError('auth.forbidden', `Role '${effectiveRole.name || 'unknown'}' denied '${permission}'.`));
}
} catch (error) {
console.error('حدث خطأ أثناء التحقق من الصلاحيات:', error);
next(error);
}
};
}
const METHOD_MAP = {
@ -134,16 +105,13 @@ const METHOD_MAP = {
* @return {import("express").RequestHandler} Express middleware function.
*/
function checkCrudPermissions(name) {
return (req, res, next) => {
// Dynamically determine the permission name (e.g., 'READ_USERS')
const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`;
// Call the checkPermissions middleware with the determined permission
checkPermissions(permissionName)(req, res, next);
};
return (req, res, next) => {
const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`;
checkPermissions(permissionName)(req, res, next);
};
}
module.exports = {
checkPermissions,
checkCrudPermissions,
checkPermissions,
checkCrudPermissions,
};

View File

@ -13,8 +13,8 @@ const loadRolesModules = () => {
RolesDBApi: require('../db/api/roles'),
};
} catch (error) {
console.error('Roles modules are missing. Advanced roles are required for this endpoint.', error);
const err = new Error('Roles modules are missing. Advanced roles are required for this endpoint.');
console.error('تعذر تحميل وحدات الأدوار المطلوبة لهذا المسار.', error);
const err = new Error('تعذر تحميل وحدات الأدوار المطلوبة لهذا المسار.');
err.originalError = error;
throw err;
}
@ -310,7 +310,7 @@ router.post(
if (!prompt) {
return res.status(400).send({
success: false,
error: 'Prompt is required',
error: 'النص المطلوب للذكاء الاصطناعي فارغ',
});
}

View File

@ -19,7 +19,7 @@ router.get('/image', async (req, res) => {
const data = await response.json();
res.status(200).json(data.photos[0]);
} catch (error) {
res.status(200).json({ error: 'Failed to fetch image' });
res.status(200).json({ error: 'تعذر جلب الصورة' });
}
});
@ -37,7 +37,7 @@ router.get('/video', async (req, res) => {
const data = await response.json();
res.status(200).json(data.videos[0]);
} catch (error) {
res.status(200).json({ error: 'Failed to fetch video' });
res.status(200).json({ error: 'تعذر جلب الفيديو' });
}
});

38
backend/src/routes/pos.js Normal file
View File

@ -0,0 +1,38 @@
const express = require('express');
const PosService = require('../services/pos');
const wrapAsync = require('../helpers').wrapAsync;
const { checkPermissions } = require('../middlewares/check-permissions');
const router = express.Router();
router.get(
'/workspace',
checkPermissions('READ_PRODUCTS'),
wrapAsync(async (req, res) => {
const payload = await PosService.getWorkspace(req.currentUser, req.query.shopId);
res.status(200).send(payload);
}),
);
router.post(
'/checkout',
checkPermissions('CREATE_SALES_INVOICES'),
wrapAsync(async (req, res) => {
const payload = await PosService.checkout(req.currentUser, req.body);
res.status(200).send(payload);
}),
);
router.post(
'/pricing',
checkPermissions('UPDATE_SHOPS'),
wrapAsync(async (req, res) => {
const payload = await PosService.updatePricing(req.currentUser, req.body);
res.status(200).send(payload);
}),
);
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

View File

@ -41,15 +41,15 @@ router.post('/', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
if (!searchQuery) {
return res.status(400).json({ error: 'Please enter a search query' });
return res.status(400).json({ error: 'يرجى إدخال عبارة للبحث' });
}
try {
const foundMatches = await SearchService.search(searchQuery, req.currentUser , organizationId, globalAccess,);
res.json(foundMatches);
} catch (error) {
console.error('Internal Server Error', error);
res.status(500).json({ error: 'Internal Server Error' });
console.error('حدث خطأ داخلي في البحث', error);
res.status(500).json({ error: 'حدث خطأ داخلي أثناء تنفيذ البحث' });
}
});

View File

@ -38,16 +38,16 @@ router.post(
wrapAsync(async (req, res) => {
const { sql } = req.body;
if (typeof sql !== 'string' || !sql.trim()) {
return res.status(400).json({ error: 'SQL is required' });
return res.status(400).json({ error: 'يرجى إدخال استعلام SQL' });
}
const normalized = sql.trim().replace(/;+\s*$/, '');
if (!/^select\b/i.test(normalized)) {
return res.status(400).json({ error: 'Only SELECT statements are allowed' });
return res.status(400).json({ error: 'يسمح فقط باستعلامات SELECT' });
}
if (normalized.includes(';')) {
return res.status(400).json({ error: 'Only a single SELECT statement is allowed' });
return res.status(400).json({ error: 'يسمح باستعلام SELECT واحد فقط في كل طلب' });
}
const rows = await db.sequelize.query(normalized, {

View File

@ -2,7 +2,7 @@ const formidable = require('formidable');
const fs = require('fs');
const config = require('../config');
const path = require('path');
const { format } = require("util");
const { format } = require('util');
const ensureDirectoryExistence = (filePath) => {
const dirname = path.dirname(filePath);
@ -13,7 +13,7 @@ const ensureDirectoryExistence = (filePath) => {
ensureDirectoryExistence(dirname);
fs.mkdirSync(dirname);
}
};
const uploadLocal = (
folder,
@ -29,9 +29,7 @@ const uploadLocal = (
return;
}
if (
validations.entity
) {
if (validations.entity) {
res.sendStatus(403);
return;
}
@ -63,7 +61,7 @@ const uploadLocal = (
if (!filename) {
fs.unlinkSync(fileTempUrl);
res.sendStatus(500);
res.status(500).send({ message: 'تعذر تحديد اسم الملف المطلوب رفعه' });
return;
}
@ -80,59 +78,57 @@ const uploadLocal = (
form.on('error', function (err) {
res.status(500).send(err);
});
}
}
};
};
const downloadLocal = async (req, res) => {
const privateUrl = req.query.privateUrl;
if (!privateUrl) {
return res.sendStatus(404);
}
res.download(path.join(config.uploadDir, privateUrl));
}
const privateUrl = req.query.privateUrl;
if (!privateUrl) {
return res.sendStatus(404);
}
res.download(path.join(config.uploadDir, privateUrl));
};
const initGCloud = () => {
const processFile = require("../middlewares/upload");
const { Storage } = require("@google-cloud/storage");
const processFile = require('../middlewares/upload');
const { Storage } = require('@google-cloud/storage');
const crypto = require('crypto')
const hash = config.gcloud.hash
const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, "\n");
const hash = config.gcloud.hash;
const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n');
const storage = new Storage({
projectId: process.env.GC_PROJECT_ID,
credentials: {
client_email: process.env.GC_CLIENT_EMAIL,
private_key: privateKey
}
projectId: process.env.GC_PROJECT_ID,
credentials: {
client_email: process.env.GC_CLIENT_EMAIL,
private_key: privateKey,
},
});
const bucket = storage.bucket(config.gcloud.bucket);
return {hash, bucket, processFile};
}
return { hash, bucket, processFile };
};
const uploadGCloud = async (folder, req, res) => {
try {
const {hash, bucket, processFile} = initGCloud();
const { hash, bucket, processFile } = initGCloud();
await processFile(req, res);
let buffer = await req.file.buffer;
let filename = await req.body.filename;
if (!req.file) {
return res.status(400).send({ message: "Please upload a file!" });
return res.status(400).send({ message: 'يرجى اختيار ملف للرفع' });
}
let path = `${hash}/${folder}/${filename}`;
let blob = bucket.file(path);
const buffer = req.file.buffer;
const filename = req.body.filename;
const filePath = `${hash}/${folder}/${filename}`;
const blob = bucket.file(filePath);
console.log(path);
console.log(filePath);
const blobStream = blob.createWriteStream({
resumable: false,
});
blobStream.on("error", (err) => {
blobStream.on('error', (err) => {
console.log('Upload error');
console.log(err.message);
res.status(500).send({ message: err.message });
@ -140,58 +136,58 @@ const uploadGCloud = async (folder, req, res) => {
console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`);
blobStream.on("finish", async (data) => {
blobStream.on('finish', async () => {
const publicUrl = format(
`https://storage.googleapis.com/${bucket.name}/${blob.name}`
`https://storage.googleapis.com/${bucket.name}/${blob.name}`,
);
res.status(200).send({
message: "Uploaded the file successfully: " + path,
message: `تم رفع الملف بنجاح: ${filePath}`,
url: publicUrl,
});
});
blobStream.end(buffer)
blobStream.end(buffer);
} catch (err) {
console.log(err);
res.status(500).send({
message: `Could not upload the file. ${err}`
message: `تعذر رفع الملف. ${err}`,
});
}
}
};
const downloadGCloud = async (req, res) => {
try {
const {hash, bucket, processFile} = initGCloud();
const { hash, bucket } = initGCloud();
const privateUrl = await req.query.privateUrl;
const privateUrl = req.query.privateUrl;
const filePath = `${hash}/${privateUrl}`;
const file = bucket.file(filePath)
const file = bucket.file(filePath);
const fileExists = await file.exists();
if (fileExists[0]) {
const stream = file.createReadStream();
stream.pipe(res);
return;
}
else {
res.status(404).send({
message: "Could not download the file. " + err,
});
}
res.status(404).send({
message: 'تعذر العثور على الملف المطلوب تنزيله',
});
} catch (err) {
res.status(404).send({
message: "Could not download the file. " + err,
message: 'تعذر تنزيل الملف.',
});
}
}
};
const deleteGCloud = async (privateUrl) => {
try {
const {hash, bucket, processFile} = initGCloud();
const { hash, bucket } = initGCloud();
const filePath = `${hash}/${privateUrl}`;
const file = bucket.file(filePath)
const file = bucket.file(filePath);
const fileExists = await file.exists();
if (fileExists[0]) {
@ -200,7 +196,7 @@ const deleteGCloud = async (privateUrl) => {
} catch (err) {
console.log(`Cannot find the file ${privateUrl}`);
}
}
};
module.exports = {
initGCloud,
@ -208,6 +204,5 @@ module.exports = {
downloadLocal,
deleteGCloud,
uploadGCloud,
downloadGCloud
}
downloadGCloud,
};

View File

@ -1,101 +1,96 @@
const errors = {
app: {
title: 'Multi-Client Detergents POS',
title: 'نظام إدارة المحل ونقطة البيع',
},
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',
userDisabled: 'تم تعطيل هذا الحساب',
forbidden: 'ليس لديك صلاحية لتنفيذ هذا الإجراء',
unauthorized: 'يجب تسجيل الدخول أولاً',
userNotFound: 'البريد الإلكتروني أو كلمة المرور غير صحيحة',
wrongPassword: 'البريد الإلكتروني أو كلمة المرور غير صحيحة',
weakPassword: 'كلمة المرور ضعيفة جدًا',
emailAlreadyInUse: 'هذا البريد الإلكتروني مستخدم بالفعل',
invalidEmail: 'يرجى إدخال بريد إلكتروني صحيح',
passwordReset: {
invalidToken:
'Password reset link is invalid or has expired',
error: `Email not recognized`,
invalidToken: 'رابط إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية',
error: 'هذا البريد الإلكتروني غير مسجل في النظام',
},
passwordUpdate: {
samePassword: `You can't use the same password. Please create new password`
samePassword: 'لا يمكن استخدام كلمة المرور الحالية نفسها. اختر كلمة مرور جديدة',
},
userNotVerified: `Sorry, your email has not been verified yet`,
userNotVerified: 'لم يتم تأكيد البريد الإلكتروني لهذا الحساب بعد',
emailAddressVerificationEmail: {
invalidToken:
'Email verification link is invalid or has expired',
error: `Email not recognized`,
invalidToken: 'رابط تأكيد البريد الإلكتروني غير صالح أو منتهي الصلاحية',
error: 'هذا البريد الإلكتروني غير مسجل في النظام',
},
},
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',
userAlreadyExists: 'يوجد مستخدم بهذا البريد الإلكتروني بالفعل',
userNotFound: 'المستخدم غير موجود',
disablingHimself: 'لا يمكنك تعطيل حسابك الحالي',
revokingOwnPermission: 'لا يمكنك إزالة صلاحية المالك من حسابك الحالي',
deletingHimself: 'لا يمكنك حذف حسابك الحالي',
emailRequired: 'البريد الإلكتروني مطلوب',
},
},
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',
invalidFileEmpty: 'الملف فارغ',
invalidFileExcel: 'يسمح فقط بملفات Excel بصيغة .xlsx',
invalidFileUpload: 'الملف غير صالح. تأكد من استخدام آخر نسخة من القالب المعتمد.',
importHashRequired: 'معرّف الاستيراد مطلوب',
importHashExistent: 'تم استيراد هذه البيانات مسبقًا',
userEmailMissing: 'بعض الصفوف في ملف CSV لا تحتوي على بريد إلكتروني',
},
},
errors: {
forbidden: {
message: 'Forbidden',
message: 'ليس لديك صلاحية للوصول',
},
validation: {
message: 'An error occurred',
message: 'حدث خطأ في التحقق من البيانات',
},
searchQueryRequired: {
message: 'Search query is required',
message: 'يرجى إدخال عبارة البحث',
},
},
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>
subject: 'تمت دعوتك إلى {0}',
body: `
<p>مرحبًا،</p>
<p>تمت دعوتك إلى {0}. يرجى تعيين كلمة المرور لحسابك في {1}.</p>
<p><a href='{2}'>{2}</a></p>
<p>Thanks,</p>
<p>Your {0} team</p>
<p>شكرًا لك،</p>
<p>فريق {0}</p>
`,
},
emailAddressVerification: {
subject: `Verify your email for {0}`,
subject: 'تأكيد البريد الإلكتروني لـ {0}',
body: `
<p>Hello,</p>
<p>Follow this link to verify your email address.</p>
<p>مرحبًا،</p>
<p>استخدم هذا الرابط لتأكيد عنوان بريدك الإلكتروني.</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>
<p>إذا لم تطلب هذا الإجراء، يمكنك تجاهل هذه الرسالة.</p>
<p>شكرًا لك،</p>
<p>فريق {1}</p>
`,
},
passwordReset: {
subject: `Reset your password for {0}`,
subject: 'إعادة تعيين كلمة المرور لـ {0}',
body: `
<p>Hello,</p>
<p>Follow this link to reset your {0} password for your {1} account.</p>
<p>مرحبًا،</p>
<p>استخدم هذا الرابط لإعادة تعيين كلمة المرور الخاصة بك في {0} لحساب {1}.</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>
<p>إذا لم تطلب إعادة التعيين، يمكنك تجاهل هذه الرسالة.</p>
<p>شكرًا لك،</p>
<p>فريق {0}</p>
`,
},
},

View File

@ -6,8 +6,8 @@ const loadRoleService = () => {
try {
return require('./roles');
} catch (error) {
console.error('Role service is missing. Advanced roles are required for this operation.', error);
const err = new Error('Role service is missing. Advanced roles are required for this operation.');
console.error('تعذر تحميل خدمة الأدوار المطلوبة لهذه العملية.', error);
const err = new Error('تعذر تحميل خدمة الأدوار المطلوبة لهذه العملية.');
err.originalError = error;
throw err;
}
@ -25,17 +25,17 @@ module.exports = class OpenAiService {
const { widget_id } = await response.data;
await RoleService.addRoleInfo(roleId, userId, 'widgets', widget_id);
return widget_id;
} else {
console.error('=======error=======', response.data);
return { value: null, error: response.data };
}
console.error('Widget generation failed:', response.data);
return { value: null, error: response.data };
}
static async askGpt(prompt) {
if (!prompt) {
return {
success: false,
error: 'Prompt is required'
error: 'النص المطلوب للذكاء الاصطناعي فارغ',
};
}
@ -59,7 +59,7 @@ module.exports = class OpenAiService {
console.error('AI JSON decode failed:', error);
return {
success: false,
error: 'AI response parsing failed',
error: 'تعذر قراءة رد الذكاء الاصطناعي',
details: error.message || String(error),
};
}
@ -73,7 +73,7 @@ module.exports = class OpenAiService {
console.error('AI proxy error:', response);
return {
success: false,
error: response.error || response.message || 'AI proxy error',
error: response.error || response.message || 'حدث خطأ أثناء التواصل مع خدمة الذكاء الاصطناعي',
response,
};
}

560
backend/src/services/pos.js Normal file
View File

@ -0,0 +1,560 @@
const db = require('../db/models');
const ValidationError = require('./notifications/errors/validation');
const { Op } = db.Sequelize;
const PAYMENT_METHODS = new Set(['cash', 'card', 'transfer', 'mixed']);
const PRICING_ACTIONS = new Set(['set_rate', 'apply_prices', 'restore_prices']);
const toNumber = (value, fallback = 0) => {
const parsed = Number.parseFloat(String(value ?? ''));
return Number.isFinite(parsed) ? parsed : fallback;
};
const roundMoney = (value) => Number(toNumber(value).toFixed(2));
const formatInvoiceNumber = () => {
const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
const suffix = Math.floor(Math.random() * 900 + 100);
return `INV-${stamp}-${suffix}`;
};
const buildOrgWhere = (currentUser) => {
if (currentUser?.app_role?.globalAccess) {
return {};
}
if (!currentUser?.organizationsId) {
return { id: null };
}
return {
organizationsId: currentUser.organizationsId,
};
};
const buildShopWhere = (currentUser, shopId) => {
const orgWhere = buildOrgWhere(currentUser);
if (orgWhere.id === null) {
return orgWhere;
}
return {
...orgWhere,
...(shopId ? { id: shopId } : {}),
};
};
const getStartAndEndOfToday = () => {
const start = new Date();
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + 1);
return { start, end };
};
const mapInvoice = (invoice) => ({
id: invoice.id,
invoice_number: invoice.invoice_number,
sold_at: invoice.sold_at,
total_amount: roundMoney(invoice.total_amount),
total_profit_amount: roundMoney(invoice.total_profit_amount),
payment_method: invoice.payment_method,
item_count: (invoice.sales_invoice_items_invoice || []).reduce(
(sum, item) => sum + toNumber(item.quantity, 0),
0,
),
cashier_name: [invoice.cashier?.firstName, invoice.cashier?.lastName]
.filter(Boolean)
.join(' ')
.trim(),
});
module.exports = class PosService {
static async getWorkspace(currentUser, shopId) {
const shops = await db.shops.findAll({
where: buildShopWhere(currentUser),
attributes: [
'id',
'shop_name',
'currency_name',
'usd_rate',
'allow_negative_stock',
'is_active',
'organizationsId',
],
order: [['createdAt', 'ASC']],
});
if (!shops.length) {
return {
shops: [],
selectedShop: null,
categories: [],
products: [],
summary: {
totalSales: 0,
totalProfit: 0,
invoiceCount: 0,
},
recentInvoices: [],
latestPriceChange: null,
};
}
const selectedShop =
shops.find((shop) => shop.id === shopId) ||
shops[0];
const categories = await db.categories.findAll({
where: {
shopId: selectedShop.id,
...buildOrgWhere(currentUser),
},
attributes: ['id', 'category_name', 'description'],
order: [
['sort_order', 'ASC'],
['category_name', 'ASC'],
],
});
const products = await db.products.findAll({
where: {
shopId: selectedShop.id,
...buildOrgWhere(currentUser),
},
attributes: [
'id',
'product_name',
'sku',
'barcode',
'cost_price',
'sale_price',
'sale_price_backup',
'usd_price',
'stock_quantity',
'is_active',
'categoryId',
],
include: [
{
model: db.categories,
as: 'category',
attributes: ['id', 'category_name'],
},
],
order: [
['categoryId', 'ASC'],
['product_name', 'ASC'],
],
});
const { start, end } = getStartAndEndOfToday();
const invoiceWhere = {
shopId: selectedShop.id,
...buildOrgWhere(currentUser),
sold_at: {
[Op.gte]: start,
[Op.lt]: end,
},
status: 'paid',
};
const summaryInvoices = await db.sales_invoices.findAll({
where: invoiceWhere,
attributes: ['id', 'total_amount', 'total_profit_amount'],
});
const invoices = await db.sales_invoices.findAll({
where: invoiceWhere,
attributes: [
'id',
'invoice_number',
'sold_at',
'total_amount',
'total_profit_amount',
'payment_method',
],
include: [
{
model: db.sales_invoice_items,
as: 'sales_invoice_items_invoice',
attributes: ['id', 'quantity'],
},
{
model: db.users,
as: 'cashier',
attributes: ['id', 'firstName', 'lastName'],
},
],
order: [['sold_at', 'DESC']],
limit: 12,
});
const summary = summaryInvoices.reduce(
(acc, invoice) => ({
totalSales: acc.totalSales + toNumber(invoice.total_amount),
totalProfit: acc.totalProfit + toNumber(invoice.total_profit_amount),
invoiceCount: acc.invoiceCount + 1,
}),
{ totalSales: 0, totalProfit: 0, invoiceCount: 0 },
);
const latestPriceChange = await db.price_change_logs.findOne({
where: {
shopId: selectedShop.id,
...buildOrgWhere(currentUser),
},
order: [
['changed_at', 'DESC'],
['createdAt', 'DESC'],
],
});
return {
shops: shops.map((shop) => ({
id: shop.id,
shop_name: shop.shop_name,
currency_name: shop.currency_name,
usd_rate: roundMoney(shop.usd_rate),
allow_negative_stock: Boolean(shop.allow_negative_stock),
is_active: Boolean(shop.is_active),
})),
selectedShop: {
id: selectedShop.id,
shop_name: selectedShop.shop_name,
currency_name: selectedShop.currency_name,
usd_rate: roundMoney(selectedShop.usd_rate),
allow_negative_stock: Boolean(selectedShop.allow_negative_stock),
is_active: Boolean(selectedShop.is_active),
},
categories: categories.map((category) => ({
id: category.id,
category_name: category.category_name,
description: category.description,
})),
products: products.map((product) => ({
id: product.id,
product_name: product.product_name,
sku: product.sku,
barcode: product.barcode,
cost_price: roundMoney(product.cost_price),
sale_price: roundMoney(product.sale_price),
sale_price_backup: roundMoney(product.sale_price_backup),
usd_price: product.usd_price == null ? null : roundMoney(product.usd_price),
stock_quantity: product.stock_quantity,
is_active: Boolean(product.is_active),
categoryId: product.categoryId,
category_name: product.category?.category_name || 'بدون قسم',
})),
summary: {
totalSales: roundMoney(summary.totalSales),
totalProfit: roundMoney(summary.totalProfit),
invoiceCount: summary.invoiceCount,
},
recentInvoices: invoices.map(mapInvoice),
latestPriceChange: latestPriceChange
? {
id: latestPriceChange.id,
changed_at: latestPriceChange.changed_at || latestPriceChange.createdAt,
change_type: latestPriceChange.change_type,
usd_rate_before: roundMoney(latestPriceChange.usd_rate_before),
usd_rate_after: roundMoney(latestPriceChange.usd_rate_after),
summary: latestPriceChange.summary,
}
: null,
};
}
static async checkout(currentUser, payload) {
const transaction = await db.sequelize.transaction();
try {
const rawItems = Array.isArray(payload?.items) ? payload.items : [];
const normalizedItems = rawItems
.map((item) => ({
productId: item?.productId,
quantity: Number.parseInt(String(item?.quantity ?? ''), 10),
}))
.filter((item) => item.productId && Number.isInteger(item.quantity) && item.quantity > 0);
if (!normalizedItems.length) {
throw new ValidationError('errors.validation.message');
}
const paymentMethod = String(payload?.paymentMethod || 'cash');
if (!PAYMENT_METHODS.has(paymentMethod)) {
throw new ValidationError('errors.validation.message');
}
const shop = await db.shops.findOne({
where: buildShopWhere(currentUser, payload?.shopId),
transaction,
});
if (!shop) {
throw new ValidationError('errors.validation.message');
}
const uniqueProductIds = [...new Set(normalizedItems.map((item) => item.productId))];
const products = await db.products.findAll({
where: {
id: uniqueProductIds,
shopId: shop.id,
...buildOrgWhere(currentUser),
},
transaction,
});
if (products.length !== uniqueProductIds.length) {
throw new ValidationError('errors.validation.message');
}
const productMap = new Map(products.map((product) => [product.id, product]));
let subtotal = 0;
let totalCost = 0;
const lineItems = [];
for (const item of normalizedItems) {
const product = productMap.get(item.productId);
const salePrice = toNumber(product.sale_price);
const costPrice = toNumber(product.cost_price);
const stockQuantity = product.stock_quantity;
if (
!shop.allow_negative_stock &&
stockQuantity != null &&
Number.isFinite(stockQuantity) &&
stockQuantity < item.quantity
) {
throw new Error(`الكمية غير كافية للمنتج: ${product.product_name}`);
}
const lineSubtotal = roundMoney(salePrice * item.quantity);
const lineCost = roundMoney(costPrice * item.quantity);
const lineProfit = roundMoney(lineSubtotal - lineCost);
subtotal += lineSubtotal;
totalCost += lineCost;
lineItems.push({
product,
quantity: item.quantity,
lineSubtotal,
lineProfit,
});
}
const totalAmount = roundMoney(subtotal);
const totalCostAmount = roundMoney(totalCost);
const totalProfitAmount = roundMoney(totalAmount - totalCostAmount);
const soldAt = new Date();
const invoice = await db.sales_invoices.create(
{
invoice_number: formatInvoiceNumber(),
sold_at: soldAt,
status: 'paid',
subtotal_amount: totalAmount,
discount_amount: 0,
total_amount: totalAmount,
total_cost_amount: totalCostAmount,
total_profit_amount: totalProfitAmount,
payment_method: paymentMethod,
notes: payload?.notes || null,
shopId: shop.id,
cashierId: currentUser.id,
organizationsId: currentUser.organizationsId || shop.organizationsId || null,
createdById: currentUser.id,
updatedById: currentUser.id,
},
{ transaction },
);
await db.sales_invoice_items.bulkCreate(
lineItems.map((item) => ({
product_name_snapshot: item.product.product_name,
cost_price_snapshot: roundMoney(item.product.cost_price),
sale_price_snapshot: roundMoney(item.product.sale_price),
quantity: item.quantity,
line_subtotal: item.lineSubtotal,
line_profit: item.lineProfit,
invoiceId: invoice.id,
productId: item.product.id,
organizationsId: currentUser.organizationsId || shop.organizationsId || null,
createdById: currentUser.id,
updatedById: currentUser.id,
})),
{ transaction },
);
for (const item of lineItems) {
if (item.product.stock_quantity != null) {
await item.product.update(
{
stock_quantity: toNumber(item.product.stock_quantity) - item.quantity,
updatedById: currentUser.id,
},
{ transaction },
);
}
}
await transaction.commit();
return {
id: invoice.id,
invoice_number: invoice.invoice_number,
sold_at: soldAt,
total_amount: totalAmount,
total_profit_amount: totalProfitAmount,
items_count: lineItems.reduce((sum, item) => sum + item.quantity, 0),
};
} catch (error) {
await transaction.rollback();
console.error('POS checkout failed:', error);
throw error;
}
}
static async updatePricing(currentUser, payload) {
const transaction = await db.sequelize.transaction();
try {
const action = String(payload?.action || '');
if (!PRICING_ACTIONS.has(action)) {
throw new ValidationError('errors.validation.message');
}
const shop = await db.shops.findOne({
where: buildShopWhere(currentUser, payload?.shopId),
transaction,
});
if (!shop) {
throw new ValidationError('errors.validation.message');
}
const existingRate = toNumber(shop.usd_rate, 0);
const incomingRate = toNumber(payload?.usdRate, existingRate);
if ((action === 'set_rate' || action === 'apply_prices') && incomingRate <= 0) {
throw new Error('يرجى إدخال سعر دولار صحيح أكبر من صفر.');
}
const products = await db.products.findAll({
where: {
shopId: shop.id,
...buildOrgWhere(currentUser),
},
transaction,
});
if (action === 'set_rate' || action === 'apply_prices') {
await shop.update(
{
usd_rate: incomingRate,
updatedById: currentUser.id,
},
{ transaction },
);
}
let changedProducts = 0;
let message = '';
let changeType = 'usd_rate_update';
if (action === 'apply_prices') {
changeType = 'bulk_increase_by_usd';
for (const product of products) {
const currentSalePrice = toNumber(product.sale_price, 0);
const baseUsdPrice =
product.usd_price != null
? toNumber(product.usd_price, 0)
: existingRate > 0
? roundMoney(currentSalePrice / existingRate)
: 0;
const updatedSalePrice = roundMoney(baseUsdPrice * incomingRate);
await product.update(
{
sale_price_backup: currentSalePrice,
usd_price: baseUsdPrice,
sale_price: updatedSalePrice,
updatedById: currentUser.id,
},
{ transaction },
);
changedProducts += 1;
}
message = `تم تحديث أسعار البيع لعدد ${changedProducts} منتج حسب سعر الدولار.`;
}
if (action === 'restore_prices') {
changeType = 'bulk_restore_previous';
for (const product of products) {
if (product.sale_price_backup == null) {
continue;
}
await product.update(
{
sale_price: product.sale_price_backup,
sale_price_backup: null,
updatedById: currentUser.id,
},
{ transaction },
);
changedProducts += 1;
}
message = changedProducts
? `تمت إعادة ${changedProducts} سعر إلى القيمة السابقة.`
: 'لا توجد أسعار محفوظة للاسترجاع حالياً.';
}
if (action === 'set_rate') {
message = `تم حفظ سعر الدولار الجديد للمحل بنجاح.`;
}
await db.price_change_logs.create(
{
changed_at: new Date(),
change_type: changeType,
usd_rate_before: existingRate || null,
usd_rate_after: action === 'restore_prices' ? existingRate || null : incomingRate,
summary: message,
shopId: shop.id,
changed_byId: currentUser.id,
organizationsId: currentUser.organizationsId || shop.organizationsId || null,
createdById: currentUser.id,
updatedById: currentUser.id,
},
{ transaction },
);
await transaction.commit();
return {
success: true,
action,
shopId: shop.id,
usdRate: action === 'restore_prices' ? roundMoney(shop.usd_rate) : roundMoney(incomingRate),
changedProducts,
message,
};
} catch (error) {
await transaction.rollback();
console.error('POS pricing update failed:', error);
throw error;
}
}
};

10
frontend/lint-final.log Normal file
View File

@ -0,0 +1,10 @@
> lint
> eslint . --ext .ts,.tsx
/home/ubuntu/executor/workspace/frontend/src/components/Logo/index.tsx
9:5 warning Do not use `<img>` element. Use `<Image />` from `next/image` instead. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
✖ 1 problem (0 errors, 1 warning)

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./build/types/routes.d.ts" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View File

@ -63,13 +63,13 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
)
const componentClass = [
'flex cursor-pointer py-1.5 ',
'flex cursor-pointer py-1.5 font-medium transition-all duration-200 ease-out',
isDropdownList ? 'px-6 text-sm' : '',
item.color
? getButtonColor(item.color, false, true)
: `${asideMenuItemStyle}`,
isLinkActive
? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800`
? `text-black ${activeLinkColor} shadow-sm dark:text-white dark:bg-dark-800`
: '',
].join(' ');

View File

@ -3,12 +3,10 @@ import { mdiLogout, mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import Link from 'next/link';
import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import Link from 'next/link'
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
type Props = {
@ -58,17 +56,17 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
return (
<aside
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed top-0 z-40 flex h-screen overflow-hidden transition-all duration-300 ease-out`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
className={`flex flex-1 flex-col overflow-hidden border border-slate-200/70 shadow-sm transition-all duration-300 ease-out dark:bg-dark-900 dark:border-dark-700 ${asideStyle} ${corners}`}
>
<div
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
>
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
<b className="font-black">Multi-Client Detergents POS</b>
<b className="font-black tracking-[0.01em]">Multi-Client Detergents POS</b>
{organizationName && <p>{organizationName}</p>}

View File

@ -48,10 +48,13 @@ export default function BaseButton({
'justify-center',
'items-center',
'whitespace-nowrap',
'font-semibold',
'tracking-[0.01em]',
'focus:outline-none',
'transition-colors',
'transition-all',
'duration-200',
'ease-out',
'focus:ring',
'duration-150',
'border',
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
roundedFull ? 'rounded-full' : `${corners}`,
@ -60,7 +63,7 @@ export default function BaseButton({
]
if (!label && icon) {
componentClass.push('p-1')
componentClass.push('p-1.5')
} else if (small) {
componentClass.push('text-sm', roundedFull ? 'px-3 py-1' : 'p-1')
} else {
@ -69,6 +72,8 @@ export default function BaseButton({
if (disabled) {
componentClass.push(outline ? 'opacity-50' : 'opacity-70')
} else {
componentClass.push('hover:-translate-y-px', 'hover:shadow-sm', 'active:translate-y-0')
}
const componentClassString = componentClass.join(' ')

View File

@ -37,16 +37,16 @@ export default function CardBox({
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const componentClass = [
`flex dark:border-dark-700 dark:bg-dark-900`,
`flex dark:border-dark-700 dark:bg-dark-900 transition-all duration-300 ease-out motion-reduce:transition-none`,
className,
corners !== 'rounded-full'? corners : 'rounded-3xl',
flex,
isList ? '' : `${cardsStyle}`,
hasTable ? '' : `border-dark-700 dark:border-dark-700`,
hasTable ? '' : `border-dark-700 dark:border-dark-700`,
]
if (isHoverable) {
componentClass.push('hover:shadow-lg transition-shadow duration-500')
componentClass.push('hover:-translate-y-0.5 hover:shadow-lg')
}
return React.createElement(

View File

@ -33,7 +33,7 @@ const CardBoxModal = ({
const footer = (
<BaseButtons>
<BaseButton label={buttonLabel} color={buttonColor} onClick={onConfirm} />
{!!onCancel && <BaseButton label="Cancel" color={buttonColor} outline onClick={onCancel} />}
{!!onCancel && <BaseButton label="إلغاء" color={buttonColor} outline onClick={onCancel} />}
</BaseButtons>
)

View File

@ -90,7 +90,7 @@ const CardCategories = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>اسمالقسم</dt>
<dt className=' text-gray-500 dark:text-dark-600'>اسم القسم</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.category_name }
@ -114,7 +114,7 @@ const CardCategories = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>ترتيبالعرض</dt>
<dt className=' text-gray-500 dark:text-dark-600'>ترتيب العرض</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.sort_order }
@ -141,7 +141,7 @@ const CardCategories = ({
))}
{!loading && categories.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -56,7 +56,7 @@ const ListCategories = ({ categories, loading, onDelete, currentPage, numPages,
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>اسمالقسم</p>
<p className={'text-xs text-gray-500 '}>اسم القسم</p>
<p className={'line-clamp-2'}>{ item.category_name }</p>
</div>
@ -72,7 +72,7 @@ const ListCategories = ({ categories, loading, onDelete, currentPage, numPages,
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>ترتيبالعرض</p>
<p className={'text-xs text-gray-500 '}>ترتيب العرض</p>
<p className={'line-clamp-2'}>{ item.sort_order }</p>
</div>
@ -102,7 +102,7 @@ const ListCategories = ({ categories, loading, onDelete, currentPage, numPages,
))}
{!loading && categories.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -277,7 +277,7 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
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>
<div className=" text-gray-500 font-bold">التصفية</div>
<Field
className={controlClasses}
name='selectedField'
@ -311,7 +311,7 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">اختر قيمة</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +326,22 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
)?.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>
<div className=" text-gray-500 font-bold">من</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='من'
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>
<div className=" text-gray-500 font-bold">إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +356,12 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
<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'
placeholder='من'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +369,11 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +383,11 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">يحتوي على</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='أدخل قيمة'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +395,12 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">الإجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد أنك تريد حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSampleCategories = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'صف واحد' : 'عدة صفوف'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -65,7 +65,7 @@ export const loadColumns = async (
{
field: 'category_name',
headerName: 'اسمالقسم',
headerName: 'اسم القسم',
flex: 1,
minWidth: 120,
filterable: false,
@ -95,7 +95,7 @@ export const loadColumns = async (
{
field: 'sort_order',
headerName: 'ترتيبالعرض',
headerName: 'ترتيب العرض',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -42,7 +42,7 @@ const KanbanCard = ({
href={`/${entityName}/${entityName}-view/?id=${item.id}`}
className={'text-base font-semibold'}
>
{item[showFieldName] ?? 'No data'}
{item[showFieldName] ?? 'لا توجد بيانات'}
</Link>
</div>
<div className={'flex items-center justify-between'}>

View File

@ -1,5 +1,4 @@
import React from 'react';
import Link from 'next/link';
import Button from '@mui/material/Button';
import BaseIcon from './BaseIcon';
import {
@ -81,7 +80,7 @@ const ListActionsPopover = ({
href={linkView}
sx={{ justifyContent: "start" }}
>
View
عرض
</Button>
{hasUpdatePermission && (
<Button
@ -90,7 +89,7 @@ const ListActionsPopover = ({
href={linkEdit}
sx={{ justifyContent: "start" }}
>
Edit
تعديل
</Button>
)}
{hasUpdatePermission && (
@ -103,7 +102,7 @@ const ListActionsPopover = ({
}}
sx={{ justifyContent: "start" }}
>
Delete
حذف
</Button>
)}
</div>

View File

@ -35,9 +35,9 @@ export default function NavBar({ menu, className = '', children }: Props) {
return (
<nav
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 w-screen transition-all duration-300 ease-out backdrop-blur-sm lg:w-auto dark:bg-dark-800`}
>
<div className={`flex lg:items-stretch ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
<div className={`flex lg:items-stretch ${containerMaxW} ${isScrolled ? 'border-b border-pavitra-400/70 shadow-sm shadow-slate-200/60 dark:border-dark-700 dark:shadow-none' : ''}`}>
<div className="flex flex-1 items-stretch h-14">{children}</div>
<div className="flex-none items-stretch flex h-14 lg:hidden">
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
@ -47,7 +47,7 @@ export default function NavBar({ menu, className = '', children }: Props) {
<div
className={`${
isMenuNavBarActive ? 'block' : 'hidden'
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
} absolute left-0 top-14 flex max-h-screen-menu w-screen items-center overflow-y-auto ${bgColor} shadow-lg transition-all duration-300 ease-out lg:static lg:w-auto lg:flex lg:overflow-visible lg:shadow-none dark:bg-dark-800`}
>
<NavBarMenuList menu={menu} />
</div>

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
@ -10,8 +9,8 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks'
import { MenuNavBarItem } from '../interfaces'
import { setDarkMode } from '../stores/styleSlice'
import { logoutUser } from '../stores/authSlice'
import { useRouter } from 'next/router';
import ClickOutside from "./ClickOutside";
import { useRouter } from 'next/router'
import ClickOutside from './ClickOutside'
type Props = {
item: MenuNavBarItem

View File

@ -17,7 +17,7 @@ export default function NavBarItemPlain({
const navBarItemLabelStyle = useAppSelector((state) => state.style.navBarItemLabelStyle)
const navBarItemLabelHoverStyle = useAppSelector((state) => state.style.navBarItemLabelHoverStyle)
const classBase = 'items-center cursor-pointer dark:text-white dark:hover:text-slate-400'
const classBase = 'items-center cursor-pointer font-medium transition-colors duration-200 ease-out dark:text-white dark:hover:text-slate-400'
const classAddon = `${display} ${navBarItemLabelStyle} ${navBarItemLabelHoverStyle} ${
useMargin ? 'my-2 mx-3' : 'py-2 px-3'
}`

View File

@ -93,7 +93,7 @@ const CardOrganizations = ({
))}
{!loading && organizations.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -70,7 +70,7 @@ const ListOrganizations = ({ organizations, loading, onDelete, currentPage, numP
))}
{!loading && organizations.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -395,12 +395,12 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">إجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد من حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'السجل المحدد' : 'السجلات المحددة'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -49,16 +49,16 @@ export default function PasswordSetOrReset() {
return (
<>
<Head>
{isInvitation && <title>{getPageTitle('Set Password')}</title>}
{!isInvitation && <title>{getPageTitle('Reset Password')}</title>}
{isInvitation && <title>{getPageTitle('تعيين كلمة المرور')}</title>}
{!isInvitation && <title>{getPageTitle('إعادة تعيين كلمة المرور')}</title>}
</Head>
<SectionFullScreen bg='violet'>
<div className='w-full flex flex-col items-center justify-center'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
{isInvitation && <p className='text-xl mb-2'>Set Password</p>}
{!isInvitation && <p className='text-xl mb-2'>Reset Password</p>}
<p className='text-base mb-4'>Enter your new password</p>
{isInvitation && <p className='text-xl mb-2'>تعيين كلمة المرور</p>}
{!isInvitation && <p className='text-xl mb-2'>إعادة تعيين كلمة المرور</p>}
<p className='text-base mb-4'>أدخل كلمة المرور الجديدة</p>
<Formik
initialValues={{
@ -74,7 +74,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='password'
placeholder='Password'
placeholder='كلمة المرور الجديدة'
/>
</FormField>
<FormField
@ -82,7 +82,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='confirm'
placeholder='Confirm Password'
placeholder='تأكيد كلمة المرور'
/>
</FormField>
@ -93,10 +93,10 @@ export default function PasswordSetOrReset() {
disabled={loading}
label={
loading
? 'Loading...'
? 'جارٍ الحفظ...'
: isInvitation
? 'Set Password'
: 'Reset Password'
? 'تعيين كلمة المرور'
: 'إعادة تعيين كلمة المرور'
}
color='info'
/>

View File

@ -93,7 +93,7 @@ const CardPermissions = ({
))}
{!loading && permissions.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -70,7 +70,7 @@ const ListPermissions = ({ permissions, loading, onDelete, currentPage, numPages
))}
{!loading && permissions.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -395,12 +395,12 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">إجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد من حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'السجل المحدد' : 'السجلات المحددة'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -165,7 +165,7 @@ const CardPrice_change_logs = ({
))}
{!loading && price_change_logs.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -118,7 +118,7 @@ const ListPrice_change_logs = ({ price_change_logs, loading, onDelete, currentPa
))}
{!loading && price_change_logs.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -395,12 +395,12 @@ const TableSamplePrice_change_logs = ({ filterItems, setFilterItems, filters, sh
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">إجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSamplePrice_change_logs = ({ filterItems, setFilterItems, filters, sh
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSamplePrice_change_logs = ({ filterItems, setFilterItems, filters, sh
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد من حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSamplePrice_change_logs = ({ filterItems, setFilterItems, filters, sh
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'السجل المحدد' : 'السجلات المحددة'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -111,7 +111,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>اسمالمنتج</dt>
<dt className=' text-gray-500 dark:text-dark-600'>اسم المنتج</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.product_name }
@ -123,7 +123,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>رمزالمنتج</dt>
<dt className=' text-gray-500 dark:text-dark-600'>رمز المنتج</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.sku }
@ -147,7 +147,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>سعرالتكلفةالحقيقي</dt>
<dt className=' text-gray-500 dark:text-dark-600'>سعر التكلفة</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.cost_price }
@ -159,7 +159,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>سعرالبيع</dt>
<dt className=' text-gray-500 dark:text-dark-600'>سعر البيع</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.sale_price }
@ -171,7 +171,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>سعرالبيعالسابق</dt>
<dt className=' text-gray-500 dark:text-dark-600'>سعر البيع السابق</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.sale_price_backup }
@ -183,7 +183,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>سعرالتكلفةالسابق</dt>
<dt className=' text-gray-500 dark:text-dark-600'>سعر التكلفة السابق</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.cost_price_backup }
@ -195,7 +195,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>السعربالدولار</dt>
<dt className=' text-gray-500 dark:text-dark-600'>السعر بالدولار</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.usd_price }
@ -207,7 +207,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>الكميةبالمخزون</dt>
<dt className=' text-gray-500 dark:text-dark-600'>الكمية بالمخزون</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.stock_quantity }
@ -219,7 +219,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>حدالتنبيهلنقصالمخزون</dt>
<dt className=' text-gray-500 dark:text-dark-600'>حد التنبيه لنقص المخزون</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.low_stock_threshold }
@ -247,7 +247,7 @@ const CardProducts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>متاحللبيع</dt>
<dt className=' text-gray-500 dark:text-dark-600'>متاح للبيع</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.booleanFormatter(item.is_active) }
@ -262,7 +262,7 @@ const CardProducts = ({
))}
{!loading && products.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -71,7 +71,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>اسمالمنتج</p>
<p className={'text-xs text-gray-500 '}>اسم المنتج</p>
<p className={'line-clamp-2'}>{ item.product_name }</p>
</div>
@ -79,7 +79,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>رمزالمنتج</p>
<p className={'text-xs text-gray-500 '}>رمز المنتج</p>
<p className={'line-clamp-2'}>{ item.sku }</p>
</div>
@ -95,7 +95,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>سعرالتكلفةالحقيقي</p>
<p className={'text-xs text-gray-500 '}>سعر التكلفة</p>
<p className={'line-clamp-2'}>{ item.cost_price }</p>
</div>
@ -103,7 +103,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>سعرالبيع</p>
<p className={'text-xs text-gray-500 '}>سعر البيع</p>
<p className={'line-clamp-2'}>{ item.sale_price }</p>
</div>
@ -111,7 +111,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>سعرالبيعالسابق</p>
<p className={'text-xs text-gray-500 '}>سعر البيع السابق</p>
<p className={'line-clamp-2'}>{ item.sale_price_backup }</p>
</div>
@ -119,7 +119,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>سعرالتكلفةالسابق</p>
<p className={'text-xs text-gray-500 '}>سعر التكلفة السابق</p>
<p className={'line-clamp-2'}>{ item.cost_price_backup }</p>
</div>
@ -127,7 +127,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>السعربالدولار</p>
<p className={'text-xs text-gray-500 '}>السعر بالدولار</p>
<p className={'line-clamp-2'}>{ item.usd_price }</p>
</div>
@ -135,7 +135,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>الكميةبالمخزون</p>
<p className={'text-xs text-gray-500 '}>الكمية بالمخزون</p>
<p className={'line-clamp-2'}>{ item.stock_quantity }</p>
</div>
@ -143,7 +143,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>حدالتنبيهلنقصالمخزون</p>
<p className={'text-xs text-gray-500 '}>حد التنبيه لنقص المخزون</p>
<p className={'line-clamp-2'}>{ item.low_stock_threshold }</p>
</div>
@ -163,7 +163,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>متاحللبيع</p>
<p className={'text-xs text-gray-500 '}>متاح للبيع</p>
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.is_active) }</p>
</div>
@ -185,7 +185,7 @@ const ListProducts = ({ products, loading, onDelete, currentPage, numPages, onPa
))}
{!loading && products.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -277,7 +277,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
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>
<div className=" text-gray-500 font-bold">التصفية</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +301,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
القيمة
</div>
<Field
className={controlClasses}
@ -311,7 +311,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Select القيمة</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +326,22 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
)?.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>
<div className=" text-gray-500 font-bold">من</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='من'
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>
<div className=" text-gray-500 font-bold">إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +356,12 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
<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'
placeholder='من'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +369,11 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +383,11 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">يحتوي على</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='اكتب قيمة البحث'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +395,12 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">الإجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد أنك تريد حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'سطر' : 'سطور'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -87,7 +87,7 @@ export const loadColumns = async (
{
field: 'product_name',
headerName: 'اسمالمنتج',
headerName: 'اسم المنتج',
flex: 1,
minWidth: 120,
filterable: false,
@ -102,7 +102,7 @@ export const loadColumns = async (
{
field: 'sku',
headerName: 'رمزالمنتج',
headerName: 'رمز المنتج',
flex: 1,
minWidth: 120,
filterable: false,
@ -132,7 +132,7 @@ export const loadColumns = async (
{
field: 'cost_price',
headerName: 'سعرالتكلفةالحقيقي',
headerName: 'سعر التكلفة',
flex: 1,
minWidth: 120,
filterable: false,
@ -148,7 +148,7 @@ export const loadColumns = async (
{
field: 'sale_price',
headerName: 'سعرالبيع',
headerName: 'سعر البيع',
flex: 1,
minWidth: 120,
filterable: false,
@ -164,7 +164,7 @@ export const loadColumns = async (
{
field: 'sale_price_backup',
headerName: 'سعرالبيعالسابق',
headerName: 'سعر البيع السابق',
flex: 1,
minWidth: 120,
filterable: false,
@ -180,7 +180,7 @@ export const loadColumns = async (
{
field: 'cost_price_backup',
headerName: 'سعرالتكلفةالسابق',
headerName: 'سعر التكلفة السابق',
flex: 1,
minWidth: 120,
filterable: false,
@ -196,7 +196,7 @@ export const loadColumns = async (
{
field: 'usd_price',
headerName: 'السعربالدولار',
headerName: 'السعر بالدولار',
flex: 1,
minWidth: 120,
filterable: false,
@ -212,7 +212,7 @@ export const loadColumns = async (
{
field: 'stock_quantity',
headerName: 'الكميةبالمخزون',
headerName: 'الكمية بالمخزون',
flex: 1,
minWidth: 120,
filterable: false,
@ -228,7 +228,7 @@ export const loadColumns = async (
{
field: 'low_stock_threshold',
headerName: 'حدالتنبيهلنقصالمخزون',
headerName: 'حد التنبيه لنقص المخزون',
flex: 1,
minWidth: 120,
filterable: false,
@ -244,7 +244,7 @@ export const loadColumns = async (
{
field: 'product_images',
headerName: 'صورالمنتج',
headerName: 'صور المنتج',
flex: 1,
minWidth: 120,
filterable: false,
@ -265,7 +265,7 @@ export const loadColumns = async (
{
field: 'is_active',
headerName: 'متاحللبيع',
headerName: 'متاح للبيع',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -117,7 +117,7 @@ const CardRoles = ({
))}
{!loading && roles.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -86,7 +86,7 @@ const ListRoles = ({ roles, loading, onDelete, currentPage, numPages, onPageChan
))}
{!loading && roles.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -395,12 +395,12 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">إجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد من حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'السجل المحدد' : 'السجلات المحددة'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -126,7 +126,7 @@ const CardSales_invoice_items = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>سعرالبيعوقتالبيع</dt>
<dt className=' text-gray-500 dark:text-dark-600'>سعر البيعوقتالبيع</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.sale_price_snapshot }
@ -177,7 +177,7 @@ const CardSales_invoice_items = ({
))}
{!loading && sales_invoice_items.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -80,7 +80,7 @@ const ListSales_invoice_items = ({ sales_invoice_items, loading, onDelete, curre
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>سعرالبيعوقتالبيع</p>
<p className={'text-xs text-gray-500 '}>سعر البيعوقتالبيع</p>
<p className={'line-clamp-2'}>{ item.sale_price_snapshot }</p>
</div>
@ -126,7 +126,7 @@ const ListSales_invoice_items = ({ sales_invoice_items, loading, onDelete, curre
))}
{!loading && sales_invoice_items.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -395,12 +395,12 @@ const TableSampleSales_invoice_items = ({ filterItems, setFilterItems, filters,
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">إجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSampleSales_invoice_items = ({ filterItems, setFilterItems, filters,
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSampleSales_invoice_items = ({ filterItems, setFilterItems, filters,
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد من حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSampleSales_invoice_items = ({ filterItems, setFilterItems, filters,
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'السجل المحدد' : 'السجلات المحددة'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -118,7 +118,7 @@ export const loadColumns = async (
{
field: 'sale_price_snapshot',
headerName: 'سعرالبيعوقتالبيع',
headerName: 'سعر البيعوقتالبيع',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -7,6 +7,10 @@ import { Pagination } from '../Pagination';
import {saveFile} from "../../helpers/fileSaver";
import LoadingSpinner from "../LoadingSpinner";
import Link from 'next/link';
import {
formatSalesInvoicePaymentMethod,
formatSalesInvoiceStatus,
} from '../../helpers/salesInvoiceLabels';
import {hasPermission} from "../../helpers/userPermissions";
@ -102,7 +106,7 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>رقمالفاتورة</dt>
<dt className=' text-gray-500 dark:text-dark-600'>رقم الفاتورة</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.invoice_number }
@ -114,7 +118,7 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>تاريخووقتالبيع</dt>
<dt className=' text-gray-500 dark:text-dark-600'>تاريخ ووقت البيع</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.sold_at) }
@ -129,7 +133,7 @@ const CardSales_invoices = ({
<dt className=' text-gray-500 dark:text-dark-600'>الحالة</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.status }
{ formatSalesInvoiceStatus(item.status) }
</div>
</dd>
</div>
@ -138,7 +142,7 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>الإجماليقبلالخصم</dt>
<dt className=' text-gray-500 dark:text-dark-600'>الإجمالي قبل الخصم</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.subtotal_amount }
@ -162,7 +166,7 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>الإجماليالنهائي</dt>
<dt className=' text-gray-500 dark:text-dark-600'>الإجمالي النهائي</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.total_amount }
@ -174,7 +178,7 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>إجماليالتكلفة</dt>
<dt className=' text-gray-500 dark:text-dark-600'>إجمالي التكلفة</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.total_cost_amount }
@ -186,7 +190,7 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>إجماليالربح</dt>
<dt className=' text-gray-500 dark:text-dark-600'>إجمالي الربح</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.total_profit_amount }
@ -198,10 +202,10 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>طريقةالدفع</dt>
<dt className=' text-gray-500 dark:text-dark-600'>طريقة الدفع</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.payment_method }
{ formatSalesInvoicePaymentMethod(item.payment_method) }
</div>
</dd>
</div>
@ -222,7 +226,7 @@ const CardSales_invoices = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>بنودالفاتورة</dt>
<dt className=' text-gray-500 dark:text-dark-600'>بنود الفاتورة</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.sales_invoice_itemsManyListFormatter(item.items).join(', ')}
@ -237,7 +241,7 @@ const CardSales_invoices = ({
))}
{!loading && sales_invoices.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -8,6 +8,10 @@ import {useAppSelector} from "../../stores/hooks";
import {Pagination} from "../Pagination";
import LoadingSpinner from "../LoadingSpinner";
import Link from 'next/link';
import {
formatSalesInvoicePaymentMethod,
formatSalesInvoiceStatus,
} from '../../helpers/salesInvoiceLabels';
import {hasPermission} from "../../helpers/userPermissions";
@ -64,7 +68,7 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>رقمالفاتورة</p>
<p className={'text-xs text-gray-500 '}>رقم الفاتورة</p>
<p className={'line-clamp-2'}>{ item.invoice_number }</p>
</div>
@ -72,7 +76,7 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>تاريخووقتالبيع</p>
<p className={'text-xs text-gray-500 '}>تاريخ ووقت البيع</p>
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.sold_at) }</p>
</div>
@ -81,14 +85,14 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>الحالة</p>
<p className={'line-clamp-2'}>{ item.status }</p>
<p className={'line-clamp-2'}>{ formatSalesInvoiceStatus(item.status) }</p>
</div>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>الإجماليقبلالخصم</p>
<p className={'text-xs text-gray-500 '}>الإجمالي قبل الخصم</p>
<p className={'line-clamp-2'}>{ item.subtotal_amount }</p>
</div>
@ -104,7 +108,7 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>الإجماليالنهائي</p>
<p className={'text-xs text-gray-500 '}>الإجمالي النهائي</p>
<p className={'line-clamp-2'}>{ item.total_amount }</p>
</div>
@ -112,7 +116,7 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>إجماليالتكلفة</p>
<p className={'text-xs text-gray-500 '}>إجمالي التكلفة</p>
<p className={'line-clamp-2'}>{ item.total_cost_amount }</p>
</div>
@ -120,7 +124,7 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>إجماليالربح</p>
<p className={'text-xs text-gray-500 '}>إجمالي الربح</p>
<p className={'line-clamp-2'}>{ item.total_profit_amount }</p>
</div>
@ -128,8 +132,8 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>طريقةالدفع</p>
<p className={'line-clamp-2'}>{ item.payment_method }</p>
<p className={'text-xs text-gray-500 '}>طريقة الدفع</p>
<p className={'line-clamp-2'}>{ formatSalesInvoicePaymentMethod(item.payment_method) }</p>
</div>
@ -144,7 +148,7 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>بنودالفاتورة</p>
<p className={'text-xs text-gray-500 '}>بنود الفاتورة</p>
<p className={'line-clamp-2'}>{ dataFormatter.sales_invoice_itemsManyListFormatter(item.items).join(', ')}</p>
</div>
@ -166,7 +170,7 @@ const ListSales_invoices = ({ sales_invoices, loading, onDelete, currentPage, nu
))}
{!loading && sales_invoices.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -20,10 +20,20 @@ import {dataGridStyles} from "../../styles";
import KanbanBoard from '../KanbanBoard/KanbanBoard';
import axios from 'axios';
import {
formatSalesInvoicePaymentMethod,
formatSalesInvoiceStatus,
} from '../../helpers/salesInvoiceLabels';
const perPage = 10
const getEnumOptionLabel = (fieldName: string, value: string) => {
if (fieldName === 'status') return formatSalesInvoiceStatus(value);
if (fieldName === 'payment_method') return formatSalesInvoicePaymentMethod(value);
return value;
}
const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showGrid }) => {
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
@ -98,13 +108,13 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
setKanbanColumns([
{ id: "draft", label: "draft" },
{ id: "draft", label: "مسودة" },
{ id: "paid", label: "paid" },
{ id: "paid", label: "مدفوعة" },
{ id: "voided", label: "voided" },
{ id: "voided", label: "ملغاة" },
{ id: "refunded", label: "refunded" },
{ id: "refunded", label: "مسترجعة" },
]);
@ -295,7 +305,7 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
{filterItems && Array.isArray( filterItems ) && filterItems.length ?
<CardBox>
<Formik
initialValues={{
initialالقيمةs={{
checkboxes: ['lorem'],
switches: ['lorem'],
radio: 'lorem',
@ -308,7 +318,7 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
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>
<div className=" text-gray-500 font-bold">التصفية</div>
<Field
className={controlClasses}
name='selectedField'
@ -332,7 +342,7 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
القيمة
</div>
<Field
className={controlClasses}
@ -342,12 +352,12 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">اختر القيمة</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
<option key={option} value={option}>
{option}
{getEnumOptionLabel(filterItem?.fields?.selectedField, option)}
</option>
))}
</Field>
@ -357,22 +367,22 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
)?.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>
<div className=" text-gray-500 font-bold">من</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='من'
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>
<div className=" text-gray-500 font-bold">إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -387,12 +397,12 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
<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'
placeholder='من'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -400,11 +410,11 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -414,11 +424,11 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">يحتوي على</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='أدخل القيمة'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -426,12 +436,12 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">الإجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -444,13 +454,13 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -460,14 +470,14 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد أنك تريد حذف هذا العنصر؟</p>
</CardBoxModal>
@ -494,7 +504,7 @@ const TableSampleSales_invoices = ({ filterItems, setFilterItems, filters, showG
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'صف واحد' : 'عدة صفوف'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -12,6 +12,10 @@ import {saveFile} from "../../helpers/fileSaver";
import dataFormatter from '../../helpers/dataFormatter'
import DataGridMultiSelect from "../DataGridMultiSelect";
import ListActionsPopover from '../ListActionsPopover';
import {
formatSalesInvoicePaymentMethod,
formatSalesInvoiceStatus,
} from '../../helpers/salesInvoiceLabels';
import {hasPermission} from "../../helpers/userPermissions";
@ -87,7 +91,7 @@ export const loadColumns = async (
{
field: 'invoice_number',
headerName: 'رقمالفاتورة',
headerName: 'رقم الفاتورة',
flex: 1,
minWidth: 120,
filterable: false,
@ -102,7 +106,7 @@ export const loadColumns = async (
{
field: 'sold_at',
headerName: 'تاريخووقتالبيع',
headerName: 'تاريخ ووقت البيع',
flex: 1,
minWidth: 120,
filterable: false,
@ -129,13 +133,13 @@ export const loadColumns = async (
editable: hasUpdatePermission,
valueFormatter: ({ value }) => formatSalesInvoiceStatus(value),
},
{
field: 'subtotal_amount',
headerName: 'الإجماليقبلالخصم',
headerName: 'الإجمالي قبل الخصم',
flex: 1,
minWidth: 120,
filterable: false,
@ -167,7 +171,7 @@ export const loadColumns = async (
{
field: 'total_amount',
headerName: 'الإجماليالنهائي',
headerName: 'الإجمالي النهائي',
flex: 1,
minWidth: 120,
filterable: false,
@ -183,7 +187,7 @@ export const loadColumns = async (
{
field: 'total_cost_amount',
headerName: 'إجماليالتكلفة',
headerName: 'إجمالي التكلفة',
flex: 1,
minWidth: 120,
filterable: false,
@ -199,7 +203,7 @@ export const loadColumns = async (
{
field: 'total_profit_amount',
headerName: 'إجماليالربح',
headerName: 'إجمالي الربح',
flex: 1,
minWidth: 120,
filterable: false,
@ -215,7 +219,7 @@ export const loadColumns = async (
{
field: 'payment_method',
headerName: 'طريقةالدفع',
headerName: 'طريقة الدفع',
flex: 1,
minWidth: 120,
filterable: false,
@ -224,7 +228,7 @@ export const loadColumns = async (
editable: hasUpdatePermission,
valueFormatter: ({ value }) => formatSalesInvoicePaymentMethod(value),
},
@ -245,7 +249,7 @@ export const loadColumns = async (
{
field: 'items',
headerName: 'بنودالفاتورة',
headerName: 'بنود الفاتورة',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -11,9 +11,9 @@ const Search = () => {
const validateSearch = (value) => {
let error;
if (!value) {
error = 'Required';
error = 'الرجاء كتابة كلمة للبحث';
} else if (value.length < 2) {
error = 'Minimum length: 2 characters';
error = 'الحد الأدنى حرفان';
}
return error;
};
@ -31,13 +31,13 @@ const Search = () => {
validateOnChange={false}
>
{({ errors, touched, values }) => (
<Form style={{width: '300px'}} >
<Form className='w-[220px] sm:w-[300px]'>
<Field
id='search'
name='search'
validate={validateSearch}
placeholder='Search'
className={` ${corners} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-2 relative ml-2 w-full dark:placeholder-dark-600 ${focusRing} shadow-none`}
placeholder='ابحث عن صفحة أو عنصر' dir='rtl'
className={`${corners} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 relative ml-2 w-full p-2.5 text-sm font-medium shadow-none transition-all duration-200 ease-out placeholder:font-normal dark:placeholder-dark-600 ${focusRing}`}
/>
{errors.search && touched.search && values.search.length < 2 ? (
<div className='text-red-500 text-sm ml-2 absolute'>{errors.search}</div>

View File

@ -78,7 +78,7 @@ const CardShops = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>اسمالمتجر</dt>
<dt className=' text-gray-500 dark:text-dark-600'>اسم المحل</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.shop_name }
@ -90,7 +90,7 @@ const CardShops = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>اسمالمالك</dt>
<dt className=' text-gray-500 dark:text-dark-600'>اسم المالك</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.owner_name }
@ -102,7 +102,7 @@ const CardShops = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>هاتفالمتجر</dt>
<dt className=' text-gray-500 dark:text-dark-600'>هاتف المحل</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.phone }
@ -126,7 +126,7 @@ const CardShops = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>اسمالعملةالمحلية</dt>
<dt className=' text-gray-500 dark:text-dark-600'>اسم العملة المحلية</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.currency_name }
@ -138,7 +138,7 @@ const CardShops = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>سعرالدولاراليومي</dt>
<dt className=' text-gray-500 dark:text-dark-600'>سعر الدولار اليومي</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.usd_rate }
@ -150,7 +150,7 @@ const CardShops = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>السماحبمخزونسالب</dt>
<dt className=' text-gray-500 dark:text-dark-600'>السماح بمخزون سالب</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.booleanFormatter(item.allow_negative_stock) }
@ -177,7 +177,7 @@ const CardShops = ({
))}
{!loading && shops.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListShops = ({ shops, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>اسمالمتجر</p>
<p className={'text-xs text-gray-500 '}>اسم المحل</p>
<p className={'line-clamp-2'}>{ item.shop_name }</p>
</div>
@ -56,7 +56,7 @@ const ListShops = ({ shops, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>اسمالمالك</p>
<p className={'text-xs text-gray-500 '}>اسم المالك</p>
<p className={'line-clamp-2'}>{ item.owner_name }</p>
</div>
@ -64,7 +64,7 @@ const ListShops = ({ shops, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>هاتفالمتجر</p>
<p className={'text-xs text-gray-500 '}>هاتف المحل</p>
<p className={'line-clamp-2'}>{ item.phone }</p>
</div>
@ -80,7 +80,7 @@ const ListShops = ({ shops, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>اسمالعملةالمحلية</p>
<p className={'text-xs text-gray-500 '}>اسم العملة المحلية</p>
<p className={'line-clamp-2'}>{ item.currency_name }</p>
</div>
@ -88,7 +88,7 @@ const ListShops = ({ shops, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>سعرالدولاراليومي</p>
<p className={'text-xs text-gray-500 '}>سعر الدولار اليومي</p>
<p className={'line-clamp-2'}>{ item.usd_rate }</p>
</div>
@ -96,7 +96,7 @@ const ListShops = ({ shops, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>السماحبمخزونسالب</p>
<p className={'text-xs text-gray-500 '}>السماح بمخزون سالب</p>
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.allow_negative_stock) }</p>
</div>
@ -126,7 +126,7 @@ const ListShops = ({ shops, loading, onDelete, currentPage, numPages, onPageChan
))}
{!loading && shops.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -279,7 +279,7 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
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>
<div className=" text-gray-500 font-bold">التصفية</div>
<Field
className={controlClasses}
name='selectedField'
@ -313,7 +313,7 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">اختر قيمة</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -328,22 +328,22 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.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>
<div className=" text-gray-500 font-bold">من</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='من'
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>
<div className=" text-gray-500 font-bold">إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -363,7 +363,7 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='من'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -371,11 +371,11 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>إلى</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='إلى'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -385,11 +385,11 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">يحتوي على</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='يحتوي على'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -397,12 +397,12 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">الإجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -415,13 +415,13 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -431,14 +431,14 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد أنك تريد حذف هذا العنصر؟</p>
</CardBoxModal>
@ -463,7 +463,7 @@ const TableSampleShops = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'صف واحد' : 'عدة صفوف'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'shop_name',
headerName: 'اسمالمتجر',
headerName: 'اسم المحل',
flex: 1,
minWidth: 120,
filterable: false,
@ -58,7 +58,7 @@ export const loadColumns = async (
{
field: 'owner_name',
headerName: 'اسمالمالك',
headerName: 'اسم المالك',
flex: 1,
minWidth: 120,
filterable: false,
@ -73,7 +73,7 @@ export const loadColumns = async (
{
field: 'phone',
headerName: 'هاتفالمتجر',
headerName: 'هاتف المحل',
flex: 1,
minWidth: 120,
filterable: false,
@ -103,7 +103,7 @@ export const loadColumns = async (
{
field: 'currency_name',
headerName: 'اسمالعملةالمحلية',
headerName: 'اسم العملة المحلية',
flex: 1,
minWidth: 120,
filterable: false,
@ -118,7 +118,7 @@ export const loadColumns = async (
{
field: 'usd_rate',
headerName: 'سعرالدولاراليومي',
headerName: 'سعر الدولار اليومي',
flex: 1,
minWidth: 120,
filterable: false,
@ -134,7 +134,7 @@ export const loadColumns = async (
{
field: 'allow_negative_stock',
headerName: 'السماحبمخزونسالب',
headerName: 'السماح بمخزون سالب',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -202,7 +202,7 @@ const CardUsers = ({
))}
{!loading && users.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</ul>

View File

@ -145,7 +145,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
))}
{!loading && users.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>لا توجد بيانات للعرض</p>
</div>
)}
</div>

View File

@ -395,12 +395,12 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">إجراء</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='حذف'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +413,13 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
label='تطبيق'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
label='إلغاء'
onClick={handleReset}
/>
</div>
@ -429,14 +429,14 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="تأكيد العملية"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'جارٍ الحذف...' : 'تأكيد'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
<p>هل أنت متأكد من حذف هذا العنصر؟</p>
</CardBoxModal>
@ -450,7 +450,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`حذف ${selectedRows.length === 1 ? 'السجل المحدد' : 'السجلات المحددة'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -1,3 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800&display=swap');
@import "tailwind/_base.css";
@import "tailwind/_components.css";
@import "tailwind/_utilities.css";
@ -33,3 +34,69 @@
.introjs-prevbutton{
@apply bg-transparent border border-blue-600 text-blue-600 !important;
}
:root {
--app-font-primary: 'Tajawal', 'Segoe UI', Tahoma, Arial, sans-serif;
--app-transition-fast: 180ms cubic-bezier(0.22, 1, 0.36, 1);
--app-transition-base: 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
html {
scroll-behavior: smooth;
}
html, body {
font-family: var(--app-font-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
text-rendering: optimizeLegibility;
}
body,
button,
input,
select,
textarea {
font-family: inherit;
}
h1,
h2,
h3,
h4,
h5,
h6 {
letter-spacing: -0.01em;
}
a,
button,
input,
select,
textarea {
transition:
color var(--app-transition-fast),
background-color var(--app-transition-fast),
border-color var(--app-transition-fast),
box-shadow var(--app-transition-fast),
opacity var(--app-transition-fast),
transform var(--app-transition-fast);
}
.app-rtl {
direction: rtl;
text-align: right;
}
.app-rtl input,
.app-rtl textarea,
.app-rtl select {
direction: rtl;
}
.app-rtl .ltr-chip {
direction: ltr;
}

View File

@ -0,0 +1,23 @@
export const SALES_INVOICE_STATUS_LABELS: Record<string, string> = {
draft: 'مسودة',
paid: 'مدفوعة',
voided: 'ملغاة',
refunded: 'مسترجعة',
};
export const SALES_INVOICE_PAYMENT_METHOD_LABELS: Record<string, string> = {
cash: 'نقداً',
card: 'بطاقة',
transfer: 'تحويل',
mixed: 'مختلط',
};
export const formatSalesInvoiceStatus = (value?: string | null) => {
if (!value) return 'لا توجد بيانات';
return SALES_INVOICE_STATUS_LABELS[value] ?? value;
};
export const formatSalesInvoicePaymentMethod = (value?: string | null) => {
if (!value) return 'لا توجد بيانات';
return SALES_INVOICE_PAYMENT_METHOD_LABELS[value] ?? value;
};

View File

@ -1,6 +1,5 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import jwt from 'jsonwebtoken';
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken'
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
import menuNavBar from '../menuNavBar'
@ -10,11 +9,11 @@ import NavBarItemPlain from '../components/NavBarItemPlain'
import AsideMenu from '../components/AsideMenu'
import FooterBar from '../components/FooterBar'
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Search from '../components/Search';
import Search from '../components/Search'
import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice";
import { findMe, logoutUser } from '../stores/authSlice'
import {hasPermission} from "../helpers/userPermissions";
import { hasPermission } from '../helpers/userPermissions'
type Props = {
@ -122,7 +121,7 @@ export default function LayoutAuthenticated({
onAsideLgClose={() => setIsAsideLgActive(false)}
/>
{children}
<FooterBar>Hand-crafted & Made with </FooterBar>
<FooterBar>واجهة مبسطة لإدارة المحل</FooterBar>
</div>
</div>
)

View File

@ -5,102 +5,53 @@ const menuAside: MenuAsideItem[] = [
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
{
href: '/users/users-list',
label: 'Users',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable,
permissions: 'READ_USERS'
label: 'الصفحة الرئيسية',
},
{
href: '/roles/roles-list',
label: 'Roles',
href: '/cashier',
label: 'الكاشير',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
permissions: 'READ_ROLES'
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
},
{
href: '/organizations/organizations-list',
label: 'Organizations',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ORGANIZATIONS'
},
{
href: '/shops/shops-list',
label: 'Shops',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiStorefront' in icon ? icon['mdiStorefront' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_SHOPS'
},
{
href: '/categories/categories-list',
label: 'Categories',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiShape' in icon ? icon['mdiShape' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_CATEGORIES'
icon: 'mdiCashRegister' in icon ? icon['mdiCashRegister' as keyof typeof icon] : ('mdiReceipt' in icon ? icon['mdiReceipt' as keyof typeof icon] : icon.mdiTable),
permissions: 'READ_PRODUCTS',
},
{
href: '/products/products-list',
label: 'Products',
label: 'المنتجات',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiSprayBottle' in icon ? icon['mdiSprayBottle' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PRODUCTS'
icon: 'mdiSprayBottle' in icon ? icon['mdiSprayBottle' as keyof typeof icon] : icon.mdiTable,
permissions: 'READ_PRODUCTS',
},
{
href: '/categories/categories-list',
label: 'الأقسام',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiShape' in icon ? icon['mdiShape' as keyof typeof icon] : icon.mdiTable,
permissions: 'READ_CATEGORIES',
},
{
href: '/sales_invoices/sales_invoices-list',
label: 'Sales invoices',
label: 'الفواتير',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiReceipt' in icon ? icon['mdiReceipt' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_SALES_INVOICES'
icon: 'mdiReceipt' in icon ? icon['mdiReceipt' as keyof typeof icon] : icon.mdiTable,
permissions: 'READ_SALES_INVOICES',
},
{
href: '/sales_invoice_items/sales_invoice_items-list',
label: 'Sales invoice items',
href: '/shops/shops-list',
label: 'المحل',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_SALES_INVOICE_ITEMS'
},
{
href: '/price_change_logs/price_change_logs-list',
label: 'Price change logs',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCurrencyUsd' in icon ? icon['mdiCurrencyUsd' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PRICE_CHANGE_LOGS'
icon: 'mdiStorefront' in icon ? icon['mdiStorefront' as keyof typeof icon] : icon.mdiTable,
permissions: 'READ_SHOPS',
},
{
href: '/profile',
label: 'Profile',
label: 'الملف الشخصي',
icon: icon.mdiAccountCircle,
},
{
href: '/api-docs',
target: '_blank',
label: 'Swagger API',
icon: icon.mdiFileCode,
permissions: 'READ_API_DOCS'
withDevider: true,
},
]

View File

@ -19,7 +19,7 @@ const menuNavBar: MenuNavBarItem[] = [
menu: [
{
icon: mdiAccount,
label: 'My Profile',
label: 'الملف الشخصي',
href: '/profile',
},
{
@ -27,20 +27,20 @@ const menuNavBar: MenuNavBarItem[] = [
},
{
icon: mdiLogout,
label: 'Log Out',
label: 'تسجيل الخروج',
isLogout: true,
},
],
},
{
icon: mdiThemeLightDark,
label: 'Light/Dark',
label: 'الوضع الليلي',
isDesktopNoLabel: true,
isToggleLightDark: true,
},
{
icon: mdiLogout,
label: 'Log out',
label: 'خروج',
isDesktopNoLabel: true,
isLogout: true,
},

View File

@ -0,0 +1,748 @@
import { mdiChartTimelineVariant } from '@mdi/js';
import Head from 'next/head';
import Link from 'next/link';
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
import axios from 'axios';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import LoadingSpinner from '../components/LoadingSpinner';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { formatSalesInvoicePaymentMethod } from '../helpers/salesInvoiceLabels';
import { hasPermission } from '../helpers/userPermissions';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
type CartItem = {
productId: string;
quantity: number;
};
type WorkspaceData = {
shops: any[];
selectedShop: any;
categories: any[];
products: any[];
summary: {
totalSales: number;
totalProfit: number;
invoiceCount: number;
};
recentInvoices: any[];
latestPriceChange: any;
};
const formatMoney = (value: number) => `${new Intl.NumberFormat('ar-IQ').format(value || 0)} د.ع`;
const formatUsd = (value: number | null) => {
if (value == null || Number.isNaN(value)) {
return '--';
}
return `${value.toFixed(2)} $`;
};
const formatDateTime = (value?: string | Date) => {
if (!value) {
return '--';
}
return new Date(value).toLocaleString('ar-IQ', {
hour: '2-digit',
minute: '2-digit',
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
const initialWorkspace: WorkspaceData = {
shops: [],
selectedShop: null,
categories: [],
products: [],
summary: {
totalSales: 0,
totalProfit: 0,
invoiceCount: 0,
},
recentInvoices: [],
latestPriceChange: null,
};
const CashierPage = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const corners = useAppSelector((state) => state.style.corners);
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const [workspace, setWorkspace] = useState<WorkspaceData>(initialWorkspace);
const [selectedShopId, setSelectedShopId] = useState('');
const [query, setQuery] = useState('');
const [activeCategoryId, setActiveCategoryId] = useState('all');
const [cart, setCart] = useState<CartItem[]>([]);
const [paymentMethod, setPaymentMethod] = useState('cash');
const [notes, setNotes] = useState('');
const [usdRateInput, setUsdRateInput] = useState('');
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [pricingBusy, setPricingBusy] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [successInvoice, setSuccessInvoice] = useState<any>(null);
const canCheckout = Boolean(currentUser && hasPermission(currentUser, 'CREATE_SALES_INVOICES'));
const canManagePricing = Boolean(currentUser && hasPermission(currentUser, 'UPDATE_SHOPS'));
const canCreateProducts = Boolean(currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS'));
const canCreateShops = Boolean(currentUser && hasPermission(currentUser, 'CREATE_SHOPS'));
const loadWorkspace = useCallback(
async (shopId?: string) => {
setLoading(true);
setErrorMessage('');
try {
const { data } = await axios.get('/pos/workspace', {
params: shopId ? { shopId } : undefined,
});
setWorkspace(data);
const resolvedShopId = shopId || data.selectedShop?.id || '';
setSelectedShopId(resolvedShopId);
setUsdRateInput(data.selectedShop?.usd_rate ? String(data.selectedShop.usd_rate) : '');
} catch (error: any) {
console.error('POS workspace load failed:', error);
setErrorMessage(error?.response?.data || 'تعذر تحميل شاشة الكاشير حالياً.');
} finally {
setLoading(false);
}
},
[],
);
useEffect(() => {
loadWorkspace();
}, [loadWorkspace]);
useEffect(() => {
setSuccessInvoice(null);
}, [selectedShopId]);
const productMap = useMemo(
() =>
new Map(
(workspace.products || []).map((product) => [product.id, product]),
),
[workspace.products],
);
const filteredProducts = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
return (workspace.products || []).filter((product) => {
const matchesCategory = activeCategoryId === 'all' || product.categoryId === activeCategoryId;
const searchableText = [product.product_name, product.sku, product.barcode, product.category_name]
.filter(Boolean)
.join(' ')
.toLowerCase();
const matchesQuery = !normalizedQuery || searchableText.includes(normalizedQuery);
return matchesCategory && matchesQuery;
});
}, [activeCategoryId, query, workspace.products]);
const suggestions = useMemo(() => filteredProducts.slice(0, 8), [filteredProducts]);
const cartDetails = useMemo(() => {
return cart
.map((item) => {
const product = productMap.get(item.productId);
if (!product) {
return null;
}
const lineTotal = (product.sale_price || 0) * item.quantity;
const lineProfit = ((product.sale_price || 0) - (product.cost_price || 0)) * item.quantity;
return {
...item,
product,
lineTotal,
lineProfit,
};
})
.filter(Boolean) as any[];
}, [cart, productMap]);
const cartSummary = useMemo(() => {
return cartDetails.reduce(
(acc, item) => ({
quantity: acc.quantity + item.quantity,
total: acc.total + item.lineTotal,
profit: acc.profit + item.lineProfit,
}),
{ quantity: 0, total: 0, profit: 0 },
);
}, [cartDetails]);
const addProductToCart = (productId: string) => {
setCart((current) => {
const existing = current.find((item) => item.productId === productId);
if (existing) {
return current.map((item) =>
item.productId === productId ? { ...item, quantity: item.quantity + 1 } : item,
);
}
return [...current, { productId, quantity: 1 }];
});
setSuccessInvoice(null);
};
const updateCartQuantity = (productId: string, nextQuantity: number) => {
setCart((current) =>
current
.map((item) => (item.productId === productId ? { ...item, quantity: nextQuantity } : item))
.filter((item) => item.quantity > 0),
);
};
const handleCheckout = async () => {
if (!selectedShopId || !cart.length) {
setErrorMessage('أضف منتجاً واحداً على الأقل قبل حفظ الفاتورة.');
return;
}
setSubmitting(true);
setErrorMessage('');
try {
const { data } = await axios.post('/pos/checkout', {
shopId: selectedShopId,
paymentMethod,
notes,
items: cart.map((item) => ({
productId: item.productId,
quantity: item.quantity,
})),
});
setSuccessInvoice(data);
setCart([]);
setNotes('');
setQuery('');
setActiveCategoryId('all');
await loadWorkspace(selectedShopId);
} catch (error: any) {
console.error('POS checkout failed:', error);
setErrorMessage(error?.response?.data || 'حدث خطأ أثناء حفظ الفاتورة.');
} finally {
setSubmitting(false);
}
};
const handlePricingAction = async (action: 'set_rate' | 'apply_prices' | 'restore_prices') => {
if (!selectedShopId) {
return;
}
setPricingBusy(true);
setErrorMessage('');
setSuccessInvoice(null);
try {
const payload: Record<string, string> = {
shopId: selectedShopId,
action,
};
if (action !== 'restore_prices') {
payload.usdRate = usdRateInput;
}
const { data } = await axios.post('/pos/pricing', payload);
await loadWorkspace(selectedShopId);
setErrorMessage('');
setSuccessInvoice({
invoice_number: data.message,
total_amount: 0,
total_profit_amount: 0,
});
} catch (error: any) {
console.error('POS pricing action failed:', error);
setErrorMessage(error?.response?.data || 'تعذر تنفيذ تحديث الأسعار.');
} finally {
setPricingBusy(false);
}
};
const emptyProductState = (
<CardBox className="border-dashed border-2 border-sky-100 bg-white/80">
<div className="space-y-3 py-6 text-center text-slate-600">
<p className="text-lg font-bold text-slate-900">لا توجد منتجات جاهزة للبيع بعد</p>
<p>ابدأ بإضافة منتجات وأقسام من لوحة الإدارة ليظهر الكاشير بشكل كامل.</p>
<div className="flex flex-wrap items-center justify-center gap-3">
{canCreateProducts && <BaseButton href="/products/products-new" color="success" label="إضافة منتج" />}
<BaseButton href="/products/products-list" color="info" label="عرض المنتجات" />
</div>
</div>
</CardBox>
);
return (
<>
<Head>
<title>{getPageTitle('الكاشير')}</title>
</Head>
<SectionMain>
<div className="app-rtl space-y-6" dir="rtl">
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="شاشة الكاشير وتقارير اليوم" main>
{''}
</SectionTitleLineWithButton>
<CardBox className="overflow-hidden border-0 bg-gradient-to-l from-emerald-500 via-emerald-600 to-sky-500 text-white shadow-xl shadow-emerald-100/70">
<div className="grid gap-6 px-2 py-2 lg:grid-cols-[1.35fr,0.65fr] lg:items-center">
<div className="space-y-3">
<span className="inline-flex items-center rounded-full bg-white/20 px-4 py-1 text-sm font-bold">
نظام بيع عربي سريع لمحل المنظفات
</span>
<div>
<h1 className="text-3xl font-extrabold leading-tight lg:text-4xl">بيع أسرع، فواتير أوضح، وربح يومي محسوب بدقة</h1>
<p className="mt-3 max-w-3xl text-base text-emerald-50 lg:text-lg">
ابحث عن المنتج فوراً، أضفه للفاتورة بضغطة واحدة، وراقب المبيعات والأرباح اليومية مع تحديث أسعار الدولار من نفس الشاشة.
</p>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
<div className="rounded-2xl bg-white/14 p-4 backdrop-blur-sm">
<div className="text-sm text-emerald-50">مبيعات اليوم</div>
<div className="mt-2 text-2xl font-extrabold">{formatMoney(workspace.summary.totalSales)}</div>
</div>
<div className="rounded-2xl bg-white/14 p-4 backdrop-blur-sm">
<div className="text-sm text-emerald-50">أرباح اليوم</div>
<div className="mt-2 text-2xl font-extrabold">{formatMoney(workspace.summary.totalProfit)}</div>
</div>
<div className="rounded-2xl bg-white/14 p-4 backdrop-blur-sm">
<div className="text-sm text-emerald-50">عدد الفواتير</div>
<div className="mt-2 text-2xl font-extrabold">{workspace.summary.invoiceCount}</div>
</div>
</div>
</div>
</CardBox>
{errorMessage ? (
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{errorMessage}</div>
) : null}
{successInvoice ? (
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-800">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="font-bold text-emerald-900">تمت العملية بنجاح</p>
<p className="mt-1">
{successInvoice.id ? `تم إنشاء الفاتورة رقم ${successInvoice.invoice_number}.` : successInvoice.invoice_number}
</p>
</div>
{successInvoice.id ? (
<Link
href={`/sales_invoices/sales_invoices-view/?id=${successInvoice.id}`}
className="font-bold text-emerald-700 underline decoration-emerald-300 underline-offset-4"
>
فتح تفاصيل الفاتورة
</Link>
) : null}
</div>
</div>
) : null}
{loading ? (
<CardBox>
<LoadingSpinner />
</CardBox>
) : !workspace.shops.length ? (
<CardBox>
<div className="space-y-4 py-8 text-center">
<h2 className="text-2xl font-bold text-slate-900">لا يوجد محل مرتبط بحسابك بعد</h2>
<p className="text-slate-600">أنشئ أول محل ليتم تفعيل شاشة الكاشير وتقارير اليوم.</p>
<div className="flex flex-wrap items-center justify-center gap-3">
{canCreateShops && <BaseButton href="/shops/shops-new" color="success" label="إضافة محل" />}
<BaseButton href="/shops/shops-list" color="info" label="عرض المحلات" />
</div>
</div>
</CardBox>
) : (
<div className="grid gap-6 xl:grid-cols-[1.35fr,0.65fr]">
<div className="space-y-6">
<CardBox className="border-0 bg-white shadow-lg shadow-sky-100/60">
<div className="grid gap-4 lg:grid-cols-[0.7fr,1.3fr] lg:items-end">
<div>
<label className="mb-2 block text-sm font-bold text-slate-700">المحل الحالي</label>
<select
value={selectedShopId}
onChange={(event) => loadWorkspace(event.target.value)}
className={`h-12 w-full border border-slate-200 bg-white px-4 text-right text-slate-800 transition ${focusRing} ${corners}`}
>
{(workspace.shops || []).map((shop) => (
<option key={shop.id} value={shop.id}>
{shop.shop_name}
</option>
))}
</select>
</div>
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-2xl border border-slate-100 bg-slate-50 px-4 py-3">
<div className="text-xs font-bold text-slate-500">العملة</div>
<div className="mt-1 text-lg font-bold text-slate-900">{workspace.selectedShop?.currency_name || 'دينار عراقي'}</div>
</div>
<div className="rounded-2xl border border-slate-100 bg-slate-50 px-4 py-3">
<div className="text-xs font-bold text-slate-500">سعر الدولار الحالي</div>
<div className="mt-1 text-lg font-bold text-slate-900">{workspace.selectedShop?.usd_rate || 0}</div>
</div>
<div className="rounded-2xl border border-slate-100 bg-slate-50 px-4 py-3">
<div className="text-xs font-bold text-slate-500">آخر تحديث</div>
<div className="mt-1 text-sm font-bold text-slate-900">{formatDateTime(workspace.latestPriceChange?.changed_at)}</div>
</div>
</div>
</div>
</CardBox>
<CardBox className="border-0 bg-white shadow-lg shadow-sky-100/60">
<div className="space-y-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="text-xl font-bold text-slate-900">بحث سريع عن المنتجات</h2>
<p className="text-sm text-slate-500">اكتب اسم المنتج أو الباركود أو رمز المنتج، وستظهر النتائج فوراً بدون إعادة تحميل.</p>
</div>
<div className="flex flex-wrap gap-2">
<BaseButton href="/products/products-list" color="info" label="إدارة المنتجات" />
<BaseButton href="/categories/categories-list" color="info" label="إدارة الأقسام" />
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[1.3fr,0.7fr]">
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="ابحث باسم المنتج أو الباركود..."
className={`h-14 w-full border border-slate-200 bg-slate-50 px-4 text-right text-lg text-slate-900 transition ${focusRing} ${corners}`}
/>
<div className="rounded-2xl border border-sky-100 bg-sky-50 px-4 py-3 text-sm text-sky-800">
<div className="font-bold">اقتراحات مباشرة</div>
<div className="mt-2 flex flex-wrap gap-2">
{suggestions.length ? (
suggestions.map((product) => (
<button
key={product.id}
type="button"
onClick={() => addProductToCart(product.id)}
className="rounded-full bg-white px-3 py-1.5 font-bold text-slate-700 transition hover:-translate-y-0.5 hover:text-emerald-700"
>
{product.product_name}
</button>
))
) : (
<span>ابدأ الكتابة لعرض الاقتراحات.</span>
)}
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setActiveCategoryId('all')}
className={`rounded-full px-4 py-2 text-sm font-bold transition ${
activeCategoryId === 'all'
? 'bg-emerald-600 text-white shadow-lg shadow-emerald-100'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
كل الأقسام
</button>
{(workspace.categories || []).map((category) => (
<button
key={category.id}
type="button"
onClick={() => setActiveCategoryId(category.id)}
className={`rounded-full px-4 py-2 text-sm font-bold transition ${
activeCategoryId === category.id
? 'bg-sky-600 text-white shadow-lg shadow-sky-100'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
{category.category_name}
</button>
))}
</div>
</div>
</CardBox>
{(workspace.products || []).length ? (
<div className="grid gap-4 sm:grid-cols-2 2xl:grid-cols-3">
{filteredProducts.map((product) => {
const dollarPrice = product.usd_price ?? ((product.sale_price || 0) / (workspace.selectedShop?.usd_rate || 1));
const lowStock = product.stock_quantity != null && product.stock_quantity <= 3;
return (
<button
key={product.id}
type="button"
onClick={() => addProductToCart(product.id)}
className="group rounded-3xl border border-slate-100 bg-white p-5 text-right shadow-md shadow-slate-100/70 transition duration-200 hover:-translate-y-1 hover:border-emerald-200 hover:shadow-xl"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-lg font-extrabold text-slate-900">{product.product_name}</div>
<div className="mt-1 text-sm text-slate-500">{product.category_name || 'بدون قسم'}</div>
</div>
<span className={`rounded-full px-3 py-1 text-xs font-bold ${lowStock ? 'bg-amber-100 text-amber-700' : 'bg-emerald-50 text-emerald-700'}`}>
{product.stock_quantity == null ? 'مخزون مفتوح' : `المخزون ${product.stock_quantity}`}
</span>
</div>
<div className="mt-6 grid gap-3 sm:grid-cols-2">
<div className="rounded-2xl bg-slate-50 px-3 py-3">
<div className="text-xs font-bold text-slate-500">سعر البيع</div>
<div className="mt-1 text-xl font-extrabold text-slate-900">{formatMoney(product.sale_price || 0)}</div>
</div>
<div className="rounded-2xl bg-sky-50 px-3 py-3">
<div className="text-xs font-bold text-sky-600">السعر بالدولار</div>
<div className="mt-1 text-xl font-extrabold text-sky-900">{formatUsd(dollarPrice)}</div>
</div>
</div>
<div className="mt-4 flex items-center justify-between text-sm text-slate-500">
<span>{product.sku || product.barcode || 'منتج سريع البيع'}</span>
<span className="font-bold text-emerald-700 transition group-hover:text-emerald-800">أضف للفاتورة</span>
</div>
</button>
);
})}
</div>
) : (
emptyProductState
)}
</div>
<div className="space-y-6">
<CardBox className="border-0 bg-white shadow-lg shadow-emerald-100/60">
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold text-slate-900">الفاتورة الحالية</h2>
<p className="text-sm text-slate-500">أزرار كبيرة وواضحة مناسبة للاستخدام داخل المحل.</p>
</div>
<div className="space-y-3">
{cartDetails.length ? (
cartDetails.map((item) => (
<div key={item.productId} className="rounded-2xl border border-slate-100 bg-slate-50 p-4">
<div className="flex items-start justify-between gap-4">
<div>
<div className="font-bold text-slate-900">{item.product.product_name}</div>
<div className="mt-1 text-sm text-slate-500">{item.product.category_name}</div>
</div>
<button
type="button"
onClick={() => updateCartQuantity(item.productId, 0)}
className="text-sm font-bold text-red-500 transition hover:text-red-700"
>
حذف
</button>
</div>
<div className="mt-4 flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => updateCartQuantity(item.productId, item.quantity - 1)}
className="h-10 w-10 rounded-2xl bg-white text-xl font-bold text-slate-700 shadow-sm transition hover:bg-slate-100"
>
-
</button>
<div className="min-w-14 rounded-2xl bg-white px-3 py-2 text-center text-lg font-extrabold text-slate-900 shadow-sm">
{item.quantity}
</div>
<button
type="button"
onClick={() => updateCartQuantity(item.productId, item.quantity + 1)}
className="h-10 w-10 rounded-2xl bg-emerald-600 text-xl font-bold text-white shadow-lg shadow-emerald-100 transition hover:bg-emerald-700"
>
+
</button>
</div>
<div className="text-left">
<div className="text-sm text-slate-500">إجمالي السطر</div>
<div className="text-lg font-extrabold text-slate-900">{formatMoney(item.lineTotal)}</div>
</div>
</div>
</div>
))
) : (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-slate-500">
اختر منتجات من القائمة لتكوين الفاتورة.
</div>
)}
</div>
<div className="rounded-3xl bg-slate-900 p-5 text-white shadow-xl shadow-slate-200/80">
<div className="grid gap-3 text-sm">
<div className="flex items-center justify-between">
<span className="text-slate-300">عدد القطع</span>
<span className="text-xl font-extrabold">{cartSummary.quantity}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-300">الإجمالي</span>
<span className="text-2xl font-extrabold text-emerald-300">{formatMoney(cartSummary.total)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-300">الربح المتوقع</span>
<span className="text-lg font-bold text-sky-300">{formatMoney(cartSummary.profit)}</span>
</div>
</div>
</div>
<div>
<label className="mb-2 block text-sm font-bold text-slate-700">طريقة الدفع</label>
<select
value={paymentMethod}
onChange={(event) => setPaymentMethod(event.target.value)}
className={`h-12 w-full border border-slate-200 bg-white px-4 text-right text-slate-800 transition ${focusRing} ${corners}`}
>
<option value="cash">نقدي</option>
<option value="card">بطاقة</option>
<option value="transfer">تحويل</option>
<option value="mixed">مختلط</option>
</select>
</div>
<div>
<label className="mb-2 block text-sm font-bold text-slate-700">ملاحظات الفاتورة</label>
<textarea
value={notes}
onChange={(event) => setNotes(event.target.value)}
placeholder="مثال: زبون دائم - طلب سريع"
className={`min-h-28 w-full border border-slate-200 bg-white px-4 py-3 text-right text-slate-800 transition ${focusRing} ${corners}`}
/>
</div>
<BaseButton
color="success"
label={submitting ? 'جارٍ حفظ الفاتورة...' : canCheckout ? 'تأكيد البيع وحفظ الفاتورة' : 'لا تملك صلاحية إنشاء الفواتير'}
onClick={handleCheckout}
disabled={submitting || !cartDetails.length || !canCheckout}
className="!flex h-14 w-full !items-center !justify-center text-lg font-bold"
/>
</div>
</CardBox>
<CardBox className="border-0 bg-white shadow-lg shadow-sky-100/60">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-slate-900">أدوات سعر الدولار</h2>
<p className="text-sm text-slate-500">تحديث سعر اليوم ثم تطبيق الزيادة أو الرجوع للسعر السابق.</p>
</div>
<span className="rounded-full bg-sky-50 px-3 py-1 text-xs font-bold text-sky-700">
آخر حركة: {workspace.latestPriceChange?.summary || 'لا توجد حركات بعد'}
</span>
</div>
<div>
<label className="mb-2 block text-sm font-bold text-slate-700">سعر الدولار اليومي</label>
<input
value={usdRateInput}
onChange={(event) => setUsdRateInput(event.target.value)}
placeholder="مثال: 1470"
className={`h-12 w-full border border-slate-200 bg-white px-4 text-right text-slate-800 transition ${focusRing} ${corners}`}
/>
</div>
<div className="grid gap-3">
<BaseButton
color="info"
label={pricingBusy ? 'جارٍ الحفظ...' : 'حفظ سعر الدولار'}
onClick={() => handlePricingAction('set_rate')}
disabled={pricingBusy || !canManagePricing}
className="!flex h-12 w-full !items-center !justify-center font-bold"
/>
<BaseButton
color="success"
label={pricingBusy ? 'جارٍ تحديث الأسعار...' : 'تطبيق الأسعار حسب الدولار'}
onClick={() => handlePricingAction('apply_prices')}
disabled={pricingBusy || !canManagePricing}
className="!flex h-12 w-full !items-center !justify-center font-bold"
/>
<BaseButton
color="warning"
label={pricingBusy ? 'جارٍ الاسترجاع...' : 'إرجاع الأسعار السابقة'}
onClick={() => handlePricingAction('restore_prices')}
disabled={pricingBusy || !canManagePricing}
className="!flex h-12 w-full !items-center !justify-center font-bold"
/>
</div>
{!canManagePricing ? <p className="text-xs text-slate-500">هذه الأدوات متاحة لمدير المحل أو من يملك صلاحية تحديث المحلات.</p> : null}
</div>
</CardBox>
<CardBox className="border-0 bg-white shadow-lg shadow-slate-100/80">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-slate-900">فواتير اليوم</h2>
<p className="text-sm text-slate-500">قائمة مباشرة بآخر الفواتير المدفوعة مع الربح المحسوب.</p>
</div>
<BaseButton href="/sales_invoices/sales_invoices-list" color="info" label="كل الفواتير" />
</div>
<div className="space-y-3">
{(workspace.recentInvoices || []).length ? (
workspace.recentInvoices.map((invoice) => (
<Link
key={invoice.id}
href={`/sales_invoices/sales_invoices-view/?id=${invoice.id}`}
className="block rounded-2xl border border-slate-100 bg-slate-50 px-4 py-4 transition hover:-translate-y-0.5 hover:border-emerald-200 hover:bg-white"
>
<div className="flex items-start justify-between gap-4">
<div>
<div className="font-extrabold text-slate-900">{invoice.invoice_number}</div>
<div className="mt-1 text-sm text-slate-500">{formatDateTime(invoice.sold_at)}</div>
</div>
<span className="rounded-full bg-white px-3 py-1 text-xs font-bold text-slate-600 shadow-sm">
{formatSalesInvoicePaymentMethod(invoice.payment_method)}
</span>
</div>
<div className="mt-4 grid gap-2 sm:grid-cols-3">
<div>
<div className="text-xs text-slate-500">إجمالي الفاتورة</div>
<div className="font-bold text-slate-900">{formatMoney(invoice.total_amount)}</div>
</div>
<div>
<div className="text-xs text-slate-500">الربح</div>
<div className="font-bold text-emerald-700">{formatMoney(invoice.total_profit_amount)}</div>
</div>
<div>
<div className="text-xs text-slate-500">عدد القطع</div>
<div className="font-bold text-slate-900">{invoice.item_count}</div>
</div>
</div>
</Link>
))
) : (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-slate-500">
لم تُسجَّل أي فاتورة مدفوعة اليوم بعد.
</div>
)}
</div>
</div>
</CardBox>
</div>
</div>
)}
</div>
</SectionMain>
</>
);
};
CashierPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission="READ_PRODUCTS">{page}</LayoutAuthenticated>;
};
export default CashierPage;

View File

@ -249,10 +249,10 @@ const EditCategories = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit categories')}</title>
<title>{getPageTitle('تعديل القسم')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit categories'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل القسم'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -330,11 +330,11 @@ const EditCategories = () => {
<FormField
label="اسمالقسم"
label="اسم القسم"
>
<Field
name="category_name"
placeholder="اسمالقسم"
placeholder="اسم القسم"
/>
</FormField>
@ -405,12 +405,12 @@ const EditCategories = () => {
<FormField
label="ترتيبالعرض"
label="ترتيب العرض"
>
<Field
type="number"
name="sort_order"
placeholder="ترتيبالعرض"
placeholder="ترتيب العرض"
/>
</FormField>
@ -491,7 +491,7 @@ const EditCategories = () => {
<FormField label='organizations' labelFor='organizations'>
<FormField label='المنظمة' labelFor='organizations'>
<Field
name='organizations'
id='organizations'
@ -538,9 +538,9 @@ const EditCategories = () => {
<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('/categories/categories-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/categories/categories-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -246,10 +246,10 @@ const EditCategoriesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit categories')}</title>
<title>{getPageTitle('تعديل القسم')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit categories'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل القسم'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -327,11 +327,11 @@ const EditCategoriesPage = () => {
<FormField
label="اسمالقسم"
label="اسم القسم"
>
<Field
name="category_name"
placeholder="اسمالقسم"
placeholder="اسم القسم"
/>
</FormField>
@ -402,12 +402,12 @@ const EditCategoriesPage = () => {
<FormField
label="ترتيبالعرض"
label="ترتيب العرض"
>
<Field
type="number"
name="sort_order"
placeholder="ترتيبالعرض"
placeholder="ترتيب العرض"
/>
</FormField>
@ -488,7 +488,7 @@ const EditCategoriesPage = () => {
<FormField label='organizations' labelFor='organizations'>
<FormField label='المنظمة' labelFor='organizations'>
<Field
name='organizations'
id='organizations'
@ -535,9 +535,9 @@ const EditCategoriesPage = () => {
<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('/categories/categories-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/categories/categories-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -34,8 +34,8 @@ const CategoriesTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'اسمالقسم', title: 'category_name'},{label: 'الوصف', title: 'description'},
{label: 'ترتيبالعرض', title: 'sort_order', number: 'true'},
const [filters] = useState([{label: 'اسم القسم', title: 'category_name'},{label: 'الوصف', title: 'description'},
{label: 'ترتيب العرض', title: 'sort_order', number: 'true'},
@ -90,28 +90,28 @@ const CategoriesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Categories')}</title>
<title>{getPageTitle('الأقسام')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Categories" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="الأقسام" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/categories/categories-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/categories/categories-new'} color='info' label='إضافة قسم'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='إضافة تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getCategoriesCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getCategoriesCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -133,9 +133,9 @@ const CategoriesTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='رفع CSV'
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={'تأكيد'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -142,10 +142,10 @@ const CategoriesNew = () => {
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('إضافة قسم')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="إضافة قسم" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -192,11 +192,11 @@ const CategoriesNew = () => {
<FormField
label="اسمالقسم"
label="اسم القسم"
>
<Field
name="category_name"
placeholder="اسمالقسم"
placeholder="اسم القسم"
/>
</FormField>
@ -263,12 +263,12 @@ const CategoriesNew = () => {
<FormField
label="ترتيبالعرض"
label="ترتيب العرض"
>
<Field
type="number"
name="sort_order"
placeholder="ترتيبالعرض"
placeholder="ترتيب العرض"
/>
</FormField>
@ -344,7 +344,7 @@ const CategoriesNew = () => {
<FormField label="organizations" labelFor="organizations">
<FormField label="المنظمة" labelFor="organizations">
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
</FormField>
@ -356,9 +356,9 @@ const CategoriesNew = () => {
<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('/categories/categories-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/categories/categories-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -34,8 +34,8 @@ const CategoriesTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'اسمالقسم', title: 'category_name'},{label: 'الوصف', title: 'description'},
{label: 'ترتيبالعرض', title: 'sort_order', number: 'true'},
const [filters] = useState([{label: 'اسم القسم', title: 'category_name'},{label: 'الوصف', title: 'description'},
{label: 'ترتيب العرض', title: 'sort_order', number: 'true'},
@ -90,28 +90,28 @@ const CategoriesTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Categories')}</title>
<title>{getPageTitle('الأقسام')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Categories" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="الأقسام" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/categories/categories-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/categories/categories-new'} color='info' label='إضافة قسم'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='إضافة تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getCategoriesCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getCategoriesCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -120,7 +120,7 @@ const CategoriesTablesPage = () => {
<div id='delete-rows-button'></div>
<Link href={'/categories/categories-list'}>
Back to <span className='capitalize'>table</span>
العودة إلى <span className='capitalize'>الجدول</span>
</Link>
</div>
@ -135,9 +135,9 @@ const CategoriesTablesPage = () => {
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='رفع CSV'
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={'تأكيد'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -33,10 +33,6 @@ const CategoriesView = () => {
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
@ -46,13 +42,13 @@ const CategoriesView = () => {
return (
<>
<Head>
<title>{getPageTitle('View categories')}</title>
<title>{getPageTitle('تفاصيل القسم')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View categories')} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تفاصيل القسم'} main>
<BaseButton
color='info'
label='Edit'
label='تعديل'
href={`/categories/categories-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
@ -91,7 +87,7 @@ const CategoriesView = () => {
<p>{categories?.shop?.shop_name ?? 'No data'}</p>
<p>{categories?.shop?.shop_name ?? 'لا توجد بيانات'}</p>
@ -117,7 +113,7 @@ const CategoriesView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>اسمالقسم</p>
<p className={'block font-bold mb-2'}>اسم القسم</p>
<p>{categories?.category_name}</p>
</div>
@ -150,7 +146,7 @@ const CategoriesView = () => {
<FormField label='Multi Text' hasTextareaHeight>
<FormField label='الوصف' hasTextareaHeight>
<textarea className={'w-full'} disabled value={categories?.description} />
</FormField>
@ -186,8 +182,8 @@ const CategoriesView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ترتيبالعرض</p>
<p>{categories?.sort_order || 'No data'}</p>
<p className={'block font-bold mb-2'}>ترتيب العرض</p>
<p>{categories?.sort_order || 'لا توجد بيانات'}</p>
</div>
@ -266,7 +262,7 @@ const CategoriesView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p className={'block font-bold mb-2'}>المنظمة</p>
@ -275,7 +271,7 @@ const CategoriesView = () => {
<p>{categories?.organizations?.name ?? 'No data'}</p>
<p>{categories?.organizations?.name ?? 'لا توجد بيانات'}</p>
@ -309,7 +305,7 @@ const CategoriesView = () => {
<>
<p className={'block font-bold mb-2'}>Products القسم</p>
<p className={'block font-bold mb-2'}>منتجات القسم</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
@ -324,11 +320,11 @@ const CategoriesView = () => {
<th>اسمالمنتج</th>
<th>اسم المنتج</th>
<th>رمزالمنتج</th>
<th>رمز المنتج</th>
@ -336,37 +332,37 @@ const CategoriesView = () => {
<th>سعرالتكلفةالحقيقي</th>
<th>سعر التكلفة</th>
<th>سعرالبيع</th>
<th>سعر البيع</th>
<th>سعرالبيعالسابق</th>
<th>سعر البيع السابق</th>
<th>سعرالتكلفةالسابق</th>
<th>سعر التكلفة السابق</th>
<th>السعربالدولار</th>
<th>السعر بالدولار</th>
<th>الكميةبالمخزون</th>
<th>الكمية بالمخزون</th>
<th>حدالتنبيهلنقصالمخزون</th>
<th>حد التنبيه لنقص المخزون</th>
<th>متاحللبيع</th>
<th>متاح للبيع</th>
</tr>
@ -381,61 +377,61 @@ const CategoriesView = () => {
<td data-label="product_name">
<td data-label="اسم المنتج">
{ item.product_name }
</td>
<td data-label="sku">
<td data-label="رمز المنتج">
{ item.sku }
</td>
<td data-label="barcode">
<td data-label="الباركود">
{ item.barcode }
</td>
<td data-label="cost_price">
<td data-label="سعر التكلفة">
{ item.cost_price }
</td>
<td data-label="sale_price">
<td data-label="سعر البيع">
{ item.sale_price }
</td>
<td data-label="sale_price_backup">
<td data-label="سعر البيع السابق">
{ item.sale_price_backup }
</td>
<td data-label="cost_price_backup">
<td data-label="سعر التكلفة السابق">
{ item.cost_price_backup }
</td>
<td data-label="usd_price">
<td data-label="السعر بالدولار">
{ item.usd_price }
</td>
<td data-label="stock_quantity">
<td data-label="الكمية بالمخزون">
{ item.stock_quantity }
</td>
<td data-label="low_stock_threshold">
<td data-label="حد التنبيه لنقص المخزون">
{ item.low_stock_threshold }
</td>
@ -443,7 +439,7 @@ const CategoriesView = () => {
<td data-label="is_active">
<td data-label="الحالة">
{ dataFormatter.booleanFormatter(item.is_active) }
</td>
@ -453,7 +449,7 @@ const CategoriesView = () => {
</tbody>
</table>
</div>
{!categories?.products_category?.length && <div className={'text-center py-4'}>No data</div>}
{!categories?.products_category?.length && <div className={'text-center py-4'}>لا توجد بيانات</div>}
</CardBox>
</>
@ -466,7 +462,7 @@ const CategoriesView = () => {
<BaseButton
color='info'
label='Back'
label='رجوع'
onClick={() => router.push('/categories/categories-list')}
/>
</CardBox>

View File

@ -1,441 +1,388 @@
import * as icon from '@mdi/js';
import Head from 'next/head'
import React from 'react'
import { mdiChartTimelineVariant } from '@mdi/js';
import Head from 'next/head';
import Link from 'next/link';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import axios from 'axios';
import type { ReactElement } from 'react'
import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config'
import Link from "next/link";
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import LoadingSpinner from '../components/LoadingSpinner';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { formatSalesInvoicePaymentMethod } from '../helpers/salesInvoiceLabels';
import { hasPermission } from '../helpers/userPermissions';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
type PosSummary = {
totalSales: number;
totalProfit: number;
invoiceCount: number;
};
type PosInvoice = {
id: string;
invoice_number: string;
sold_at: string;
total_amount: number;
total_profit_amount: number;
payment_method: string;
item_count: number;
cashier_name: string;
};
type PosWorkspace = {
selectedShop: {
id: string;
shop_name: string;
currency_name: string;
usd_rate: number;
} | null;
summary: PosSummary;
recentInvoices: PosInvoice[];
};
const emptyWorkspace: PosWorkspace = {
selectedShop: null,
summary: {
totalSales: 0,
totalProfit: 0,
invoiceCount: 0,
},
recentInvoices: [],
};
const formatMoney = (value: number) => `${new Intl.NumberFormat('ar-IQ').format(value || 0)} د.ع`;
const formatNumber = (value: number) => new Intl.NumberFormat('ar-IQ').format(value || 0);
const formatDateTime = (value?: string) => {
if (!value) {
return '--';
}
return new Date(value).toLocaleString('ar-IQ', {
hour: '2-digit',
minute: '2-digit',
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const Dashboard = () => {
const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const { currentUser } = useAppSelector((state) => state.auth);
const corners = useAppSelector((state) => state.style.corners);
const loadingMessage = 'Loading...';
const [workspace, setWorkspace] = useState<PosWorkspace>(emptyWorkspace);
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [users, setUsers] = React.useState(loadingMessage);
const [roles, setRoles] = React.useState(loadingMessage);
const [permissions, setPermissions] = React.useState(loadingMessage);
const [organizations, setOrganizations] = React.useState(loadingMessage);
const [shops, setShops] = React.useState(loadingMessage);
const [categories, setCategories] = React.useState(loadingMessage);
const [products, setProducts] = React.useState(loadingMessage);
const [sales_invoices, setSales_invoices] = React.useState(loadingMessage);
const [sales_invoice_items, setSales_invoice_items] = React.useState(loadingMessage);
const [price_change_logs, setPrice_change_logs] = React.useState(loadingMessage);
const canReadProducts = hasPermission(currentUser, 'READ_PRODUCTS');
const canReadInvoices = hasPermission(currentUser, 'READ_SALES_INVOICES');
const canReadShops = hasPermission(currentUser, 'READ_SHOPS');
const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
});
const { currentUser } = useAppSelector((state) => state.auth);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
const organizationId = currentUser?.organizations?.id;
async function loadData() {
const entities = ['users','roles','permissions','organizations','shops','categories','products','sales_invoices','sales_invoice_items','price_change_logs',];
const fns = [setUsers,setRoles,setPermissions,setOrganizations,setShops,setCategories,setProducts,setSales_invoices,setSales_invoice_items,setPrice_change_logs,];
const requests = entities.map((entity, index) => {
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
return axios.get(`/${entity.toLowerCase()}/count`);
} else {
fns[index](null);
return Promise.resolve({data: {count: null}});
}
});
Promise.allSettled(requests).then((results) => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
fns[i](result.value.data.count);
} else {
fns[i](result.reason.message);
}
});
});
useEffect(() => {
if (!currentUser) {
return;
}
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
}
React.useEffect(() => {
if (!currentUser) return;
loadData().then();
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
}, [currentUser]);
React.useEffect(() => {
if (!currentUser || !widgetsRole?.role?.value) return;
getWidgets(widgetsRole?.role?.value || '').then();
}, [widgetsRole?.role?.value]);
if (!canReadProducts) {
setLoading(false);
return;
}
const loadWorkspace = async () => {
setLoading(true);
setErrorMessage('');
try {
const { data } = await axios.get('/pos/workspace');
setWorkspace({
selectedShop: data.selectedShop || null,
summary: data.summary || emptyWorkspace.summary,
recentInvoices: Array.isArray(data.recentInvoices) ? data.recentInvoices : [],
});
} catch (error: any) {
console.error('Dashboard workspace load failed:', error);
setErrorMessage(error?.response?.data || 'تعذر تحميل ملخص اليوم الآن.');
} finally {
setLoading(false);
}
};
loadWorkspace();
}, [canReadProducts, currentUser]);
const quickActions = useMemo(
() => [
{
href: '/cashier',
label: 'فتح الكاشير',
description: 'ابدأ عملية بيع جديدة بسرعة وابحث عن المنتج مباشرة.',
visible: canReadProducts,
},
{
href: '/products/products-list',
label: 'إدارة المنتجات',
description: 'أضف المنتجات وعدّل الأسعار والكميات من مكان واحد.',
visible: canReadProducts,
},
{
href: '/sales_invoices/sales_invoices-list',
label: 'مراجعة الفواتير',
description: 'تابع المبيعات اليومية وشاهد الأرباح وتفاصيل كل فاتورة.',
visible: canReadInvoices,
},
{
href: '/shops/shops-list',
label: 'إعدادات المحل',
description: 'حدّث بيانات المحل والعملة وسعر الدولار اليومي.',
visible: canReadShops,
},
].filter((item) => item.visible),
[canReadInvoices, canReadProducts, canReadShops],
);
const startSteps = useMemo(
() => [
'أضف المنتجات مع سعر التكلفة وسعر البيع والكمية.',
'افتح شاشة الكاشير واختر المنتجات المطلوبة بسرعة.',
'احفظ الفاتورة ثم راقب ملخص اليوم من هذه الصفحة.',
],
[],
);
const stats = useMemo(
() => [
{
label: 'المحل الحالي',
value: workspace.selectedShop?.shop_name || 'غير محدد بعد',
hint: workspace.selectedShop?.currency_name || 'أضف محلًا واحدًا للبدء',
cardClassName: 'border border-emerald-100 bg-white shadow-sm shadow-emerald-100/70',
},
{
label: 'مبيعات اليوم',
value: formatMoney(workspace.summary.totalSales),
hint: 'إجمالي قيمة الفواتير المدفوعة اليوم',
cardClassName: 'border border-sky-100 bg-white shadow-sm shadow-sky-100/70',
},
{
label: 'ربح اليوم',
value: formatMoney(workspace.summary.totalProfit),
hint: 'صافي الفرق بين سعر البيع وسعر التكلفة',
cardClassName: 'border border-violet-100 bg-white shadow-sm shadow-violet-100/70',
},
{
label: 'عدد الفواتير اليوم',
value: formatNumber(workspace.summary.invoiceCount),
hint: 'عدد عمليات البيع المسجلة اليوم',
cardClassName: 'border border-amber-100 bg-white shadow-sm shadow-amber-100/70',
},
],
[workspace],
);
const welcomeName = currentUser?.firstName || 'بك';
const primaryAction = quickActions[0];
const usdRate = workspace.selectedShop?.usd_rate ? formatNumber(workspace.selectedShop.usd_rate) : '--';
return (
<>
<Head>
<title>
{getPageTitle('Overview')}
</title>
<title>{getPageTitle('الواجهة الرئيسية')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Overview'
main>
icon={mdiChartTimelineVariant}
title="الواجهة الرئيسية"
main
>
{''}
</SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>}
{!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
</p>
)}
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
{(isFetchingQuery || loading) && (
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
<BaseIcon
className={`${iconsColor} animate-spin mr-5`}
w='w-16'
h='h-16'
size={48}
path={icon.mdiLoading}
/>{' '}
Loading widgets...
<div className="mb-6 grid gap-6 xl:grid-cols-[1.2fr,0.8fr]">
<CardBox className="border border-emerald-100 bg-gradient-to-l from-white via-emerald-50 to-sky-50 shadow-lg shadow-emerald-100/60">
<div className="space-y-5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<span className="inline-flex rounded-full border border-emerald-200 bg-white px-3 py-1 text-xs font-bold text-emerald-700">
ملخص سريع وواضح لعمل اليوم
</span>
<h2 className="mt-3 text-3xl font-extrabold text-slate-950">أهلاً {welcomeName}</h2>
<p className="mt-2 max-w-2xl text-sm leading-7 text-slate-600">
هذه الصفحة تجمع لك أهم ما تحتاجه: حالة المحل الحالية، أرقام اليوم، وروابط سريعة للمنتجات والكاشير والفواتير.
</p>
</div>
)}
{ rolesWidgets &&
rolesWidgets.map((widget) => (
<SmartWidget
key={widget.id}
userId={currentUser?.id}
widget={widget}
roleId={widgetsRole?.role?.value || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
/>
))}
{primaryAction ? (
<BaseButton
href={primaryAction.href}
color="success"
label={primaryAction.label}
className="!px-5 !py-3 font-bold"
/>
) : null}
</div>
<div className="grid gap-3 md:grid-cols-3">
<div className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border border-white bg-white/90 px-4 py-4`}>
<div className="text-xs font-bold text-slate-500">المحل</div>
<div className="mt-2 text-lg font-extrabold text-slate-900">{workspace.selectedShop?.shop_name || 'غير محدد بعد'}</div>
</div>
<div className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border border-white bg-white/90 px-4 py-4`}>
<div className="text-xs font-bold text-slate-500">سعر الدولار اليومي</div>
<div className="mt-2 text-lg font-extrabold text-slate-900">{usdRate}</div>
</div>
<div className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border border-white bg-white/90 px-4 py-4`}>
<div className="text-xs font-bold text-slate-500">الروابط السريعة</div>
<div className="mt-2 text-lg font-extrabold text-slate-900">{formatNumber(quickActions.length)}</div>
</div>
</div>
</div>
</CardBox>
<CardBox className="border border-slate-100 bg-white shadow-lg shadow-slate-100/70">
<div className="space-y-4">
<div>
<h3 className="text-xl font-bold text-slate-900">ابدأ من هنا</h3>
<p className="mt-1 text-sm leading-6 text-slate-500">
رتّبت الواجهة لتكون بسيطة: أضف البيانات مرة واحدة، ثم استخدم الكاشير، وبعدها راقب النتائج يوميًا.
</p>
</div>
<div className="space-y-3">
{startSteps.map((step, index) => (
<div
key={step}
className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} flex items-start gap-3 border border-slate-100 bg-slate-50 px-4 py-3`}
>
<span className="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-emerald-100 text-sm font-extrabold text-emerald-700">
{index + 1}
</span>
<p className="text-sm font-medium leading-6 text-slate-700">{step}</p>
</div>
))}
</div>
{primaryAction ? (
<BaseButton href={primaryAction.href} color="info" label={`الانتقال إلى ${primaryAction.label}`} />
) : null}
</div>
</CardBox>
</div>
{!!rolesWidgets.length && <hr className='my-6 ' />}
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-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">
Users
</div>
<div className="text-3xl leading-tight font-semibold">
{users}
</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.mdiAccountGroup || icon.mdiTable}
/>
</div>
</div>
{errorMessage ? (
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{errorMessage}
</div>
) : null}
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{stats.map((item) => (
<CardBox key={item.label} className={item.cardClassName}>
<div className="text-sm font-bold text-slate-500">{item.label}</div>
<div className="mt-2 text-2xl font-extrabold text-slate-900">{item.value}</div>
<div className="mt-1 text-xs text-slate-500">{item.hint}</div>
</CardBox>
))}
</div>
<div className="grid gap-6 xl:grid-cols-[0.92fr,1.08fr]">
<CardBox className="border border-slate-100 bg-white shadow-lg shadow-slate-100/70">
<div className="space-y-4">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-xl font-bold text-slate-900">الصفحات المهمة</h3>
<p className="mt-1 text-sm text-slate-500">
هذه أهم الروابط اليومية بعد تنظيف القائمة الجانبية وتبسيط الواجهة.
</p>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-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">
Roles
</div>
<div className="text-3xl leading-tight font-semibold">
{roles}
</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.mdiShieldAccountVariantOutline || icon.mdiTable}
/>
</div>
</div>
<span className="rounded-full bg-slate-100 px-3 py-1 text-xs font-bold text-slate-600">
{formatNumber(quickActions.length)} روابط
</span>
</div>
{!quickActions.length ? (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500">
لا توجد صفحات متاحة لك حاليًا حسب الصلاحيات.
</div>
</Link>}
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-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">
Permissions
</div>
<div className="text-3xl leading-tight font-semibold">
{permissions}
</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.mdiShieldAccountOutline || icon.mdiTable}
/>
</div>
</div>
) : (
<div className="grid gap-3">
{quickActions.map((item) => (
<Link
key={item.href}
href={item.href}
className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} block border border-slate-100 bg-slate-50 px-4 py-4 transition hover:border-emerald-200 hover:bg-emerald-50/60`}
>
<div className="text-base font-bold text-slate-900">{item.label}</div>
<div className="mt-1 text-sm leading-6 text-slate-600">{item.description}</div>
</Link>
))}
</div>
</Link>}
{hasPermission(currentUser, 'READ_ORGANIZATIONS') && <Link href={'/organizations/organizations-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">
Organizations
</div>
<div className="text-3xl leading-tight font-semibold">
{organizations}
</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>
</CardBox>
<CardBox className="border border-slate-100 bg-white shadow-lg shadow-slate-100/70">
<div className="space-y-4">
<div className="flex items-center justify-between gap-3">
<div>
<h3 className="text-xl font-bold text-slate-900">آخر فواتير اليوم</h3>
<p className="mt-1 text-sm text-slate-500">لمتابعة حركة البيع بسرعة من نفس الواجهة.</p>
</div>
</Link>}
{hasPermission(currentUser, 'READ_SHOPS') && <Link href={'/shops/shops-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">
Shops
</div>
<div className="text-3xl leading-tight font-semibold">
{shops}
</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={'mdiStorefront' in icon ? icon['mdiStorefront' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
{canReadInvoices && (
<BaseButton href="/sales_invoices/sales_invoices-list" color="info" label="كل الفواتير" small />
)}
</div>
{loading ? (
<LoadingSpinner />
) : !workspace.recentInvoices.length ? (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500">
لا توجد فواتير مدفوعة اليوم حتى الآن.
</div>
</Link>}
{hasPermission(currentUser, 'READ_CATEGORIES') && <Link href={'/categories/categories-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 className="space-y-3">
{workspace.recentInvoices.slice(0, 6).map((invoice) => (
<div
key={invoice.id}
className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border border-slate-100 bg-slate-50 px-4 py-4`}
>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Categories
</div>
<div className="text-3xl leading-tight font-semibold">
{categories}
</div>
<div className="text-sm font-bold text-slate-900">{invoice.invoice_number}</div>
<div className="mt-1 text-xs text-slate-500">
{formatDateTime(invoice.sold_at)}
{invoice.cashier_name ? `${invoice.cashier_name}` : ''}
</div>
<div className="mt-2 inline-flex rounded-full bg-white px-3 py-1 text-xs font-bold text-slate-600">
{formatSalesInvoicePaymentMethod(invoice.payment_method)}
</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={'mdiShape' in icon ? icon['mdiShape' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
<div className="grid gap-1 text-sm lg:text-left">
<div className="font-bold text-slate-900">الإجمالي: {formatMoney(invoice.total_amount)}</div>
<div className="text-slate-600">الربح: {formatMoney(invoice.total_profit_amount)}</div>
<div className="text-slate-500">عدد القطع: {formatNumber(invoice.item_count || 0)}</div>
</div>
</div>
</div>
))}
</div>
</Link>}
{hasPermission(currentUser, 'READ_PRODUCTS') && <Link href={'/products/products-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">
Products
</div>
<div className="text-3xl leading-tight font-semibold">
{products}
</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={'mdiSprayBottle' in icon ? icon['mdiSprayBottle' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_SALES_INVOICES') && <Link href={'/sales_invoices/sales_invoices-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">
Sales invoices
</div>
<div className="text-3xl leading-tight font-semibold">
{sales_invoices}
</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={'mdiReceipt' in icon ? icon['mdiReceipt' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_SALES_INVOICE_ITEMS') && <Link href={'/sales_invoice_items/sales_invoice_items-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">
Sales invoice items
</div>
<div className="text-3xl leading-tight font-semibold">
{sales_invoice_items}
</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={'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PRICE_CHANGE_LOGS') && <Link href={'/price_change_logs/price_change_logs-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">
Price change logs
</div>
<div className="text-3xl leading-tight font-semibold">
{price_change_logs}
</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={'mdiCurrencyUsd' in icon ? icon['mdiCurrencyUsd' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
)}
</div>
</CardBox>
</div>
</SectionMain>
</>
)
}
);
};
Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default Dashboard
export default Dashboard;

View File

@ -22,23 +22,23 @@ export default function Forgot() {
const handleSubmit = async (value) => {
setLoading(true)
try {
const { data: response } = await axios.post('/auth/send-password-reset-email', value);
await axios.post('/auth/send-password-reset-email', value);
setLoading(false)
notify('success', 'Please check your email for verification link');
notify('success', 'تم إرسال رابط إعادة التعيين إلى بريدك الإلكتروني.');
setTimeout(async () => {
await router.push('/login')
}, 3000)
} catch (error) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
console.error('Password reset request failed:', error)
notify('error', 'حدث خطأ. حاول مرة أخرى.')
}
};
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle('استعادة كلمة المرور')}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -50,7 +50,7 @@ export default function Forgot() {
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Please enter your email'>
<FormField label='البريد الإلكتروني' help='أدخل بريدك الإلكتروني لإرسال رابط إعادة التعيين'>
<Field name='email' />
</FormField>
@ -59,12 +59,12 @@ export default function Forgot() {
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Submit' }
label={loading ? 'جارٍ الإرسال...' : 'إرسال الرابط' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
label={'العودة لتسجيل الدخول'}
color='info'
/>
</BaseButtons>

View File

@ -1,166 +1,167 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import type { ReactElement } from 'react';
import React from 'react';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
const highlights = [
{
title: 'كاشير سريع',
text: 'بحث فوري عن المنتجات، إضافة للفاتورة بضغطة واحدة، وحساب مباشر للإجمالي.',
},
{
title: 'تسعير حسب الدولار',
text: 'حفظ سعر الدولار اليومي وتطبيق تحديث جماعي على أسعار البيع مع إمكانية الرجوع.',
},
{
title: 'تقارير يومية',
text: 'متابعة مبيعات اليوم، الأرباح، وعدد الفواتير مع تفاصيل واضحة لكل فاتورة.',
},
];
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Multi-Client Detergents POS'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
const quickLinks = [
{ href: '/login', label: 'تسجيل الدخول' },
{ href: '/dashboard', label: 'واجهة الإدارة' },
{ href: '/cashier', label: 'شاشة الكاشير' },
{ href: '/products/products-list', label: 'المنتجات' },
];
export default function HomePage() {
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('نظام إدارة محل منظفات')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Multi-Client Detergents POS app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<div className="app-rtl min-h-screen bg-[radial-gradient(circle_at_top,_rgba(16,185,129,0.16),_transparent_32%),radial-gradient(circle_at_bottom_left,_rgba(14,165,233,0.16),_transparent_28%),linear-gradient(180deg,#f8fafc_0%,#ffffff_65%)]" dir="rtl">
<header className="sticky top-0 z-20 border-b border-white/70 bg-white/80 backdrop-blur-xl">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-6 py-4">
<div>
<div className="text-xl font-extrabold text-slate-900">منظفات برو</div>
<div className="text-sm text-slate-500">منصة عربية حديثة لإدارة المبيعات والمخزون</div>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<nav className="flex flex-wrap items-center gap-2">
{quickLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="rounded-full px-4 py-2 text-sm font-bold text-slate-700 transition hover:bg-slate-100 hover:text-emerald-700"
>
{link.label}
</Link>
))}
</nav>
</div>
</header>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<main className="mx-auto flex max-w-7xl flex-col gap-10 px-6 py-10 lg:py-16">
<section className="grid gap-8 lg:grid-cols-[1.1fr,0.9fr] lg:items-center">
<div className="space-y-6">
<span className="inline-flex rounded-full border border-emerald-100 bg-emerald-50 px-4 py-2 text-sm font-bold text-emerald-700">
موقع تعريفي + نظام كاشير عربي متعدد العملاء
</span>
<div className="space-y-4">
<h1 className="text-4xl font-extrabold leading-tight text-slate-950 lg:text-6xl">
إدارة مبيعات محل المنظفات بشكل أسرع وأوضح وأجمل
</h1>
<p className="max-w-2xl text-lg leading-8 text-slate-600">
واجهة عربية بالكامل، تصميم مريح للعين، شاشة كاشير مناسبة للمحل التجاري، وتسعير ذكي حسب الدولار مع تقارير يومية وأرباح دقيقة.
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<BaseButton href="/login" color="success" label="ابدأ من لوحة الإدارة" className="!px-6 !py-3 text-base font-bold" />
<BaseButton href="/cashier" color="info" label="جرّب شاشة الكاشير" className="!px-6 !py-3 text-base font-bold" />
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div className="rounded-3xl border border-white bg-white/90 p-5 shadow-lg shadow-emerald-100/70">
<div className="text-sm font-bold text-slate-500">اللغة والاتجاه</div>
<div className="mt-2 text-2xl font-extrabold text-slate-900">عربي + RTL</div>
</div>
<div className="rounded-3xl border border-white bg-white/90 p-5 shadow-lg shadow-sky-100/70">
<div className="text-sm font-bold text-slate-500">التوسع</div>
<div className="mt-2 text-2xl font-extrabold text-slate-900">+200 منتج</div>
</div>
<div className="rounded-3xl border border-white bg-white/90 p-5 shadow-lg shadow-slate-100/80">
<div className="text-sm font-bold text-slate-500">المستخدمون</div>
<div className="mt-2 text-2xl font-extrabold text-slate-900">عدة عملاء</div>
</div>
</div>
</div>
</div>
<CardBox className="border-0 bg-slate-950 text-white shadow-2xl shadow-sky-100/60">
<div className="space-y-5 p-2">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-bold text-slate-400">المشهد الأول في النظام</div>
<div className="mt-1 text-2xl font-extrabold">شاشة كاشير عصرية للمحل</div>
</div>
<span className="rounded-full bg-white/10 px-3 py-1 text-xs font-bold text-sky-200">MVP جاهز</span>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-3xl bg-white/5 p-4 ring-1 ring-white/10">
<div className="text-sm text-slate-400">بحث فوري</div>
<div className="mt-2 text-lg font-bold">اقتراحات مباشرة بدون إعادة تحميل</div>
</div>
<div className="rounded-3xl bg-white/5 p-4 ring-1 ring-white/10">
<div className="text-sm text-slate-400">فاتورة سريعة</div>
<div className="mt-2 text-lg font-bold">إجمالي وربح وعدد قطع في نفس اللحظة</div>
</div>
<div className="rounded-3xl bg-white/5 p-4 ring-1 ring-white/10">
<div className="text-sm text-slate-400">سعر الدولار</div>
<div className="mt-2 text-lg font-bold">حفظ السعر اليومي + تطبيق جماعي + استرجاع</div>
</div>
<div className="rounded-3xl bg-white/5 p-4 ring-1 ring-white/10">
<div className="text-sm text-slate-400">تقارير اليوم</div>
<div className="mt-2 text-lg font-bold">مبيعات وأرباح وفواتير اليوم من نفس الشاشة</div>
</div>
</div>
<div className="rounded-[28px] bg-gradient-to-l from-emerald-500 to-sky-500 p-5 text-slate-950">
<div className="text-sm font-bold">الوصول السريع</div>
<div className="mt-2 text-2xl font-extrabold">لوحة الإدارة ما زالت متاحة بالكامل</div>
<p className="mt-2 text-sm leading-7 text-slate-900/80">
من هنا يمكنك دخول الواجهة الإدارية الحالية لإدارة المنتجات، الأقسام، المحلات، الفواتير، والمستخدمين بدون حذف أي شيء من البنية الجاهزة.
</p>
</div>
</div>
</CardBox>
</section>
<section className="grid gap-4 lg:grid-cols-3">
{highlights.map((item) => (
<CardBox key={item.title} className="border-0 bg-white/90 shadow-lg shadow-slate-100/80">
<div className="space-y-3">
<div className="inline-flex rounded-full bg-slate-100 px-3 py-1 text-xs font-bold text-slate-600">ميزة أساسية</div>
<h2 className="text-2xl font-extrabold text-slate-900">{item.title}</h2>
<p className="leading-8 text-slate-600">{item.text}</p>
</div>
</CardBox>
))}
</section>
<section className="rounded-[32px] border border-slate-100 bg-white/90 p-8 shadow-xl shadow-slate-100/80">
<div className="grid gap-6 lg:grid-cols-[1fr,auto] lg:items-center">
<div>
<div className="text-sm font-bold text-emerald-600">ماذا ستجد الآن؟</div>
<h2 className="mt-2 text-3xl font-extrabold text-slate-950">أول شريحة MVP تعمل من البداية للنهاية</h2>
<p className="mt-3 max-w-3xl text-lg leading-8 text-slate-600">
صفحة عامة جميلة، رابط مباشر للوحة الإدارة، وشاشة كاشير عربية تجمع البيع السريع مع تقرير اليوم وأدوات تحديث الأسعار حسب الدولار.
</p>
</div>
<div className="flex flex-wrap gap-3">
<BaseButton href="/dashboard" color="info" label="فتح واجهة الإدارة" className="!px-6 !py-3 text-base font-bold" />
<BaseButton href="/sales_invoices/sales_invoices-list" color="success" label="عرض الفواتير" className="!px-6 !py-3 text-base font-bold" />
</div>
</div>
</section>
</main>
</div>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
HomePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -44,7 +44,7 @@ export default function Login() {
password: '0897e59a',
remember: true })
const title = 'Multi-Client Detergents POS'
const title = 'نظام مبيعات محل المنظفات'
// Fetch Pexels image/video
useEffect( () => {
@ -109,8 +109,7 @@ export default function Login() {
backgroundRepeat: 'no-repeat',
}}>
<div className="flex justify-center w-full bg-blue-300/20">
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo
by {image?.photographer} on Pexels</a>
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">الصورة بعدسة {image?.photographer} عبر Pexels</a>
</div>
</div>
)
@ -126,7 +125,7 @@ export default function Login() {
muted
>
<source src={video.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
متصفحك لا يدعم تشغيل الفيديو.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
@ -135,7 +134,7 @@ export default function Login() {
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
الفيديو بواسطة {video.user.name} عبر Pexels
</a>
</div>
</div>)
@ -154,7 +153,7 @@ export default function Login() {
backgroundRepeat: 'no-repeat',
} : {}}>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle('تسجيل الدخول')}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -170,25 +169,25 @@ export default function Login() {
<div className='flex flex-row text-gray-500 justify-between'>
<div>
<p className='mb-2'>Use{' '}
<p className='mb-2'>استخدم{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="0897e59a"
onClick={(e) => setLogin(e.target)}>super_admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>0897e59a</code>{' / '}
to login as Super Admin</p>
للدخول كمدير عام</p>
<p className='mb-2'>Use{' '}
<p className='mb-2'>استخدم{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="0897e59a"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>0897e59a</code>{' / '}
to login as Admin</p>
<p>Use <code
للدخول كمدير</p>
<p>استخدم <code
className={`cursor-pointer ${textColor} `}
data-password="c72eaa09f2c5"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>c72eaa09f2c5</code>{' / '}
to login as User</p>
للدخول كمستخدم</p>
</div>
<div>
<BaseIcon
@ -210,15 +209,15 @@ export default function Login() {
>
<Form>
<FormField
label='Login'
help='Please enter your login'>
label='البريد الإلكتروني'
help='أدخل بريدك الإلكتروني'>
<Field name='email' />
</FormField>
<div className='relative'>
<FormField
label='Password'
help='Please enter your password'>
label='كلمة المرور'
help='أدخل كلمة المرور'>
<Field name='password' type={showPassword ? 'text' : 'password'} />
</FormField>
<div
@ -234,12 +233,12 @@ export default function Login() {
</div>
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<FormCheckRadio type='checkbox' label='تذكرني'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
نسيت كلمة المرور؟
</Link>
</div>
@ -249,16 +248,16 @@ export default function Login() {
<BaseButton
className={'w-full'}
type='submit'
label={isFetching ? 'Loading...' : 'Login'}
label={isFetching ? 'جارٍ تسجيل الدخول...' : 'تسجيل الدخول'}
color='info'
disabled={isFetching}
/>
</BaseButtons>
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
ليس لديك حساب بعد؟{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
إنشاء حساب جديد
</Link>
</p>
</Form>
@ -268,9 +267,9 @@ export default function Login() {
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. جميع الحقوق محفوظة</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
سياسة الخصوصية
</Link>
</div>
<ToastContainer />

View File

@ -109,10 +109,10 @@ const EditOrganizations = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit organizations')}</title>
<title>{getPageTitle('تعديل منظمة')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit organizations'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل منظمة'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -164,9 +164,9 @@ const EditOrganizations = () => {
<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('/organizations/organizations-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/organizations/organizations-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -106,10 +106,10 @@ const EditOrganizationsPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit organizations')}</title>
<title>{getPageTitle('تعديل منظمة')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit organizations'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل منظمة'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -161,9 +161,9 @@ const EditOrganizationsPage = () => {
<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('/organizations/organizations-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/organizations/organizations-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -86,28 +86,28 @@ const OrganizationsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Organizations')}</title>
<title>{getPageTitle('المنظمات')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Organizations" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="المنظمات" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/organizations/organizations-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/organizations/organizations-new'} color='info' label='إضافة منظمة'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getOrganizationsCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getOrganizationsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}

View File

@ -62,10 +62,10 @@ const OrganizationsNew = () => {
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('إضافة منظمة')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="إضافة منظمة" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -116,9 +116,9 @@ const OrganizationsNew = () => {
<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('/organizations/organizations-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/organizations/organizations-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -86,28 +86,28 @@ const OrganizationsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Organizations')}</title>
<title>{getPageTitle('المنظمات')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Organizations" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="المنظمات" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/organizations/organizations-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/organizations/organizations-new'} color='info' label='إضافة منظمة'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getOrganizationsCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getOrganizationsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}

View File

@ -33,11 +33,6 @@ const OrganizationsView = () => {
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
@ -46,13 +41,13 @@ const OrganizationsView = () => {
return (
<>
<Head>
<title>{getPageTitle('View organizations')}</title>
<title>{getPageTitle('عرض منظمة')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View organizations')} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="عرض منظمة" main>
<BaseButton
color='info'
label='Edit'
label='تعديل'
href={`/organizations/organizations-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
@ -389,7 +384,7 @@ const OrganizationsView = () => {
<th>رمزالمنتج</th>
<th>رمز المنتج</th>
@ -401,11 +396,11 @@ const OrganizationsView = () => {
<th>سعرالبيع</th>
<th>سعر البيع</th>
<th>سعرالبيعالسابق</th>
<th>سعر البيعالسابق</th>
@ -427,7 +422,7 @@ const OrganizationsView = () => {
<th>متاحللبيع</th>
<th>متاح للبيع</th>
</tr>
@ -535,11 +530,11 @@ const OrganizationsView = () => {
<th>رقمالفاتورة</th>
<th>رقم الفاتورة</th>
<th>تاريخووقتالبيع</th>
<th>تاريخ ووقت البيع</th>
@ -547,7 +542,7 @@ const OrganizationsView = () => {
<th>الإجماليقبلالخصم</th>
<th>الإجمالي قبل الخصم</th>
@ -555,19 +550,19 @@ const OrganizationsView = () => {
<th>الإجماليالنهائي</th>
<th>الإجمالي النهائي</th>
<th>إجماليالتكلفة</th>
<th>إجمالي التكلفة</th>
<th>إجماليالربح</th>
<th>إجمالي الربح</th>
<th>طريقةالدفع</th>
<th>طريقة الدفع</th>
@ -683,7 +678,7 @@ const OrganizationsView = () => {
<th>سعرالبيعوقتالبيع</th>
<th>سعر البيعوقتالبيع</th>
@ -846,7 +841,7 @@ const OrganizationsView = () => {
<BaseButton
color='info'
label='Back'
label='رجوع'
onClick={() => router.push('/organizations/organizations-list')}
/>
</CardBox>

View File

@ -109,10 +109,10 @@ const EditPermissions = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit permissions')}</title>
<title>{getPageTitle('تعديل صلاحية')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit permissions'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل صلاحية'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -164,9 +164,9 @@ const EditPermissions = () => {
<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('/permissions/permissions-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/permissions/permissions-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -106,10 +106,10 @@ const EditPermissionsPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit permissions')}</title>
<title>{getPageTitle('تعديل صلاحية')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit permissions'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل صلاحية'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -161,9 +161,9 @@ const EditPermissionsPage = () => {
<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('/permissions/permissions-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/permissions/permissions-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -86,28 +86,28 @@ const PermissionsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Permissions')}</title>
<title>{getPageTitle('الصلاحيات')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Permissions" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="الصلاحيات" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label='إضافة صلاحية'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPermissionsCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getPermissionsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}

View File

@ -62,10 +62,10 @@ const PermissionsNew = () => {
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('إضافة صلاحية')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="إضافة صلاحية" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -116,9 +116,9 @@ const PermissionsNew = () => {
<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('/permissions/permissions-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/permissions/permissions-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -86,28 +86,28 @@ const PermissionsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Permissions')}</title>
<title>{getPageTitle('الصلاحيات')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Permissions" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="الصلاحيات" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/permissions/permissions-new'} color='info' label='إضافة صلاحية'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPermissionsCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getPermissionsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}

View File

@ -33,11 +33,6 @@ const PermissionsView = () => {
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
@ -46,13 +41,13 @@ const PermissionsView = () => {
return (
<>
<Head>
<title>{getPageTitle('View permissions')}</title>
<title>{getPageTitle('عرض صلاحية')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View permissions')} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="عرض صلاحية" main>
<BaseButton
color='info'
label='Edit'
label='تعديل'
href={`/permissions/permissions-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
@ -107,7 +102,7 @@ const PermissionsView = () => {
<BaseButton
color='info'
label='Back'
label='رجوع'
onClick={() => router.push('/permissions/permissions-list')}
/>
</CardBox>

View File

@ -305,10 +305,10 @@ const EditPrice_change_logs = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit price_change_logs')}</title>
<title>{getPageTitle('تعديل سجل تغيير السعر')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit price_change_logs'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل سجل تغيير السعر'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -706,9 +706,9 @@ const EditPrice_change_logs = () => {
<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('/price_change_logs/price_change_logs-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/price_change_logs/price_change_logs-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -302,10 +302,10 @@ const EditPrice_change_logsPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit price_change_logs')}</title>
<title>{getPageTitle('تعديل سجل تغيير السعر')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit price_change_logs'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل سجل تغيير السعر'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -703,9 +703,9 @@ const EditPrice_change_logsPage = () => {
<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('/price_change_logs/price_change_logs-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/price_change_logs/price_change_logs-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -94,28 +94,28 @@ const Price_change_logsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Price_change_logs')}</title>
<title>{getPageTitle('سجلات تغيير السعر')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Price_change_logs" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="سجلات تغيير السعر" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/price_change_logs/price_change_logs-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/price_change_logs/price_change_logs-new'} color='info' label='إضافة سجل تغيير السعر'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPrice_change_logsCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getPrice_change_logsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}

View File

@ -175,10 +175,10 @@ const Price_change_logsNew = () => {
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('إضافة سجل تغيير السعر')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="إضافة سجل تغيير السعر" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -460,9 +460,9 @@ const Price_change_logsNew = () => {
<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('/price_change_logs/price_change_logs-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/price_change_logs/price_change_logs-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -94,28 +94,28 @@ const Price_change_logsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Price_change_logs')}</title>
<title>{getPageTitle('سجلات تغيير السعر')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Price_change_logs" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="سجلات تغيير السعر" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/price_change_logs/price_change_logs-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/price_change_logs/price_change_logs-new'} color='info' label='إضافة سجل تغيير السعر'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getPrice_change_logsCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getPrice_change_logsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}

View File

@ -33,11 +33,6 @@ const Price_change_logsView = () => {
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
}
useEffect(() => {
dispatch(fetch({ id }));
}, [dispatch, id]);
@ -46,13 +41,13 @@ const Price_change_logsView = () => {
return (
<>
<Head>
<title>{getPageTitle('View price_change_logs')}</title>
<title>{getPageTitle('عرض سجل تغيير السعر')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View price_change_logs')} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="عرض سجل تغيير السعر" main>
<BaseButton
color='info'
label='Edit'
label='تعديل'
href={`/price_change_logs/price_change_logs-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
@ -91,7 +86,7 @@ const Price_change_logsView = () => {
<p>{price_change_logs?.shop?.shop_name ?? 'No data'}</p>
<p>{price_change_logs?.shop?.shop_name ?? 'لا توجد بيانات'}</p>
@ -139,7 +134,7 @@ const Price_change_logsView = () => {
<p className={'block font-bold mb-2'}>تمالتغييربواسطة</p>
<p>{price_change_logs?.changed_by?.firstName ?? 'No data'}</p>
<p>{price_change_logs?.changed_by?.firstName ?? 'لا توجد بيانات'}</p>
@ -228,7 +223,7 @@ const Price_change_logsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>نوعالتغيير</p>
<p>{price_change_logs?.change_type ?? 'No data'}</p>
<p>{price_change_logs?.change_type ?? 'لا توجد بيانات'}</p>
</div>
@ -252,7 +247,7 @@ const Price_change_logsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>سعرالدولارقبل</p>
<p>{price_change_logs?.usd_rate_before || 'No data'}</p>
<p>{price_change_logs?.usd_rate_before || 'لا توجد بيانات'}</p>
</div>
@ -284,7 +279,7 @@ const Price_change_logsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>سعرالدولاربعد</p>
<p>{price_change_logs?.usd_rate_after || 'No data'}</p>
<p>{price_change_logs?.usd_rate_after || 'لا توجد بيانات'}</p>
</div>
@ -368,7 +363,7 @@ const Price_change_logsView = () => {
<p>{price_change_logs?.organizations?.name ?? 'No data'}</p>
<p>{price_change_logs?.organizations?.name ?? 'لا توجد بيانات'}</p>
@ -410,7 +405,7 @@ const Price_change_logsView = () => {
<BaseButton
color='info'
label='Back'
label='رجوع'
onClick={() => router.push('/price_change_logs/price_change_logs-list')}
/>
</CardBox>

View File

@ -501,10 +501,10 @@ const EditProducts = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit products')}</title>
<title>{getPageTitle('تعديل المنتج')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit products'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل المنتج'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -646,11 +646,11 @@ const EditProducts = () => {
<FormField
label="اسمالمنتج"
label="اسم المنتج"
>
<Field
name="product_name"
placeholder="اسمالمنتج"
placeholder="اسم المنتج"
/>
</FormField>
@ -683,11 +683,11 @@ const EditProducts = () => {
<FormField
label="رمزالمنتج"
label="رمز المنتج"
>
<Field
name="sku"
placeholder="رمزالمنتج"
placeholder="رمز المنتج"
/>
</FormField>
@ -763,12 +763,12 @@ const EditProducts = () => {
<FormField
label="سعرالتكلفةالحقيقي"
label="سعر التكلفة"
>
<Field
type="number"
name="cost_price"
placeholder="سعرالتكلفةالحقيقي"
placeholder="سعر التكلفة"
/>
</FormField>
@ -801,12 +801,12 @@ const EditProducts = () => {
<FormField
label="سعرالبيع"
label="سعر البيع"
>
<Field
type="number"
name="sale_price"
placeholder="سعرالبيع"
placeholder="سعر البيع"
/>
</FormField>
@ -839,12 +839,12 @@ const EditProducts = () => {
<FormField
label="سعرالبيعالسابق"
label="سعر البيع السابق"
>
<Field
type="number"
name="sale_price_backup"
placeholder="سعرالبيعالسابق"
placeholder="سعر البيع السابق"
/>
</FormField>
@ -877,12 +877,12 @@ const EditProducts = () => {
<FormField
label="سعرالتكلفةالسابق"
label="سعر التكلفة السابق"
>
<Field
type="number"
name="cost_price_backup"
placeholder="سعرالتكلفةالسابق"
placeholder="سعر التكلفة السابق"
/>
</FormField>
@ -915,12 +915,12 @@ const EditProducts = () => {
<FormField
label="السعربالدولار"
label="السعر بالدولار"
>
<Field
type="number"
name="usd_price"
placeholder="السعربالدولار"
placeholder="السعر بالدولار"
/>
</FormField>
@ -953,12 +953,12 @@ const EditProducts = () => {
<FormField
label="الكميةبالمخزون"
label="الكمية بالمخزون"
>
<Field
type="number"
name="stock_quantity"
placeholder="الكميةبالمخزون"
placeholder="الكمية بالمخزون"
/>
</FormField>
@ -991,12 +991,12 @@ const EditProducts = () => {
<FormField
label="حدالتنبيهلنقصالمخزون"
label="حد التنبيه لنقص المخزون"
>
<Field
type="number"
name="low_stock_threshold"
placeholder="حدالتنبيهلنقصالمخزون"
placeholder="حد التنبيه لنقص المخزون"
/>
</FormField>
@ -1046,7 +1046,7 @@ const EditProducts = () => {
<FormField>
<Field
label='صورالمنتج'
label='صور المنتج'
color='info'
icon={mdiUpload}
path={'products/product_images'}
@ -1082,7 +1082,7 @@ const EditProducts = () => {
<FormField label='متاحللبيع' labelFor='is_active'>
<FormField label='متاح للبيع' labelFor='is_active'>
<Field
name='is_active'
id='is_active'
@ -1121,7 +1121,7 @@ const EditProducts = () => {
<FormField label='organizations' labelFor='organizations'>
<FormField label='المنظمة' labelFor='organizations'>
<Field
name='organizations'
id='organizations'
@ -1168,9 +1168,9 @@ const EditProducts = () => {
<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('/products/products-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/products/products-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -498,10 +498,10 @@ const EditProductsPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit products')}</title>
<title>{getPageTitle('تعديل المنتج')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit products'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'تعديل المنتج'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -643,11 +643,11 @@ const EditProductsPage = () => {
<FormField
label="اسمالمنتج"
label="اسم المنتج"
>
<Field
name="product_name"
placeholder="اسمالمنتج"
placeholder="اسم المنتج"
/>
</FormField>
@ -680,11 +680,11 @@ const EditProductsPage = () => {
<FormField
label="رمزالمنتج"
label="رمز المنتج"
>
<Field
name="sku"
placeholder="رمزالمنتج"
placeholder="رمز المنتج"
/>
</FormField>
@ -760,12 +760,12 @@ const EditProductsPage = () => {
<FormField
label="سعرالتكلفةالحقيقي"
label="سعر التكلفة"
>
<Field
type="number"
name="cost_price"
placeholder="سعرالتكلفةالحقيقي"
placeholder="سعر التكلفة"
/>
</FormField>
@ -798,12 +798,12 @@ const EditProductsPage = () => {
<FormField
label="سعرالبيع"
label="سعر البيع"
>
<Field
type="number"
name="sale_price"
placeholder="سعرالبيع"
placeholder="سعر البيع"
/>
</FormField>
@ -836,12 +836,12 @@ const EditProductsPage = () => {
<FormField
label="سعرالبيعالسابق"
label="سعر البيع السابق"
>
<Field
type="number"
name="sale_price_backup"
placeholder="سعرالبيعالسابق"
placeholder="سعر البيع السابق"
/>
</FormField>
@ -874,12 +874,12 @@ const EditProductsPage = () => {
<FormField
label="سعرالتكلفةالسابق"
label="سعر التكلفة السابق"
>
<Field
type="number"
name="cost_price_backup"
placeholder="سعرالتكلفةالسابق"
placeholder="سعر التكلفة السابق"
/>
</FormField>
@ -912,12 +912,12 @@ const EditProductsPage = () => {
<FormField
label="السعربالدولار"
label="السعر بالدولار"
>
<Field
type="number"
name="usd_price"
placeholder="السعربالدولار"
placeholder="السعر بالدولار"
/>
</FormField>
@ -950,12 +950,12 @@ const EditProductsPage = () => {
<FormField
label="الكميةبالمخزون"
label="الكمية بالمخزون"
>
<Field
type="number"
name="stock_quantity"
placeholder="الكميةبالمخزون"
placeholder="الكمية بالمخزون"
/>
</FormField>
@ -988,12 +988,12 @@ const EditProductsPage = () => {
<FormField
label="حدالتنبيهلنقصالمخزون"
label="حد التنبيه لنقص المخزون"
>
<Field
type="number"
name="low_stock_threshold"
placeholder="حدالتنبيهلنقصالمخزون"
placeholder="حد التنبيه لنقص المخزون"
/>
</FormField>
@ -1043,7 +1043,7 @@ const EditProductsPage = () => {
<FormField>
<Field
label='صورالمنتج'
label='صور المنتج'
color='info'
icon={mdiUpload}
path={'products/product_images'}
@ -1079,7 +1079,7 @@ const EditProductsPage = () => {
<FormField label='متاحللبيع' labelFor='is_active'>
<FormField label='متاح للبيع' labelFor='is_active'>
<Field
name='is_active'
id='is_active'
@ -1118,7 +1118,7 @@ const EditProductsPage = () => {
<FormField label='organizations' labelFor='organizations'>
<FormField label='المنظمة' labelFor='organizations'>
<Field
name='organizations'
id='organizations'
@ -1165,9 +1165,9 @@ const EditProductsPage = () => {
<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('/products/products-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/products/products-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -34,9 +34,9 @@ const ProductsTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'اسمالمنتج', title: 'product_name'},{label: 'رمزالمنتج', title: 'sku'},{label: 'الباركود', title: 'barcode'},
{label: 'الكميةبالمخزون', title: 'stock_quantity', number: 'true'},{label: 'حدالتنبيهلنقصالمخزون', title: 'low_stock_threshold', number: 'true'},
{label: 'سعرالتكلفةالحقيقي', title: 'cost_price', number: 'true'},{label: 'سعرالبيع', title: 'sale_price', number: 'true'},{label: 'سعرالبيعالسابق', title: 'sale_price_backup', number: 'true'},{label: 'سعرالتكلفةالسابق', title: 'cost_price_backup', number: 'true'},{label: 'السعربالدولار', title: 'usd_price', number: 'true'},
const [filters] = useState([{label: 'اسم المنتج', title: 'product_name'},{label: 'رمز المنتج', title: 'sku'},{label: 'الباركود', title: 'barcode'},
{label: 'الكمية بالمخزون', title: 'stock_quantity', number: 'true'},{label: 'حد التنبيه لنقص المخزون', title: 'low_stock_threshold', number: 'true'},
{label: 'سعر التكلفة', title: 'cost_price', number: 'true'},{label: 'سعر البيع', title: 'sale_price', number: 'true'},{label: 'سعر البيع السابق', title: 'sale_price_backup', number: 'true'},{label: 'سعر التكلفة السابق', title: 'cost_price_backup', number: 'true'},{label: 'السعر بالدولار', title: 'usd_price', number: 'true'},
@ -94,28 +94,28 @@ const ProductsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Products')}</title>
<title>{getPageTitle('المنتجات')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Products" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="المنتجات" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label='إضافة منتج'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='إضافة تصفية'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getProductsCSV} />
<BaseButton className={'mr-3'} color='info' label='تنزيل CSV' onClick={getProductsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='رفع CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -137,9 +137,9 @@ const ProductsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='رفع ملف CSV'
buttonColor='info'
buttonLabel={'Confirm'}
buttonLabel={'تأكيد'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}

View File

@ -286,10 +286,10 @@ const ProductsNew = () => {
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('إضافة منتج')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="إضافة منتج" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -366,11 +366,11 @@ const ProductsNew = () => {
<FormField
label="اسمالمنتج"
label="اسم المنتج"
>
<Field
name="product_name"
placeholder="اسمالمنتج"
placeholder="اسم المنتج"
/>
</FormField>
@ -401,11 +401,11 @@ const ProductsNew = () => {
<FormField
label="رمزالمنتج"
label="رمز المنتج"
>
<Field
name="sku"
placeholder="رمزالمنتج"
placeholder="رمز المنتج"
/>
</FormField>
@ -477,12 +477,12 @@ const ProductsNew = () => {
<FormField
label="سعرالتكلفةالحقيقي"
label="سعر التكلفة"
>
<Field
type="number"
name="cost_price"
placeholder="سعرالتكلفةالحقيقي"
placeholder="سعر التكلفة"
/>
</FormField>
@ -513,12 +513,12 @@ const ProductsNew = () => {
<FormField
label="سعرالبيع"
label="سعر البيع"
>
<Field
type="number"
name="sale_price"
placeholder="سعرالبيع"
placeholder="سعر البيع"
/>
</FormField>
@ -549,12 +549,12 @@ const ProductsNew = () => {
<FormField
label="سعرالبيعالسابق"
label="سعر البيع السابق"
>
<Field
type="number"
name="sale_price_backup"
placeholder="سعرالبيعالسابق"
placeholder="سعر البيع السابق"
/>
</FormField>
@ -585,12 +585,12 @@ const ProductsNew = () => {
<FormField
label="سعرالتكلفةالسابق"
label="سعر التكلفة السابق"
>
<Field
type="number"
name="cost_price_backup"
placeholder="سعرالتكلفةالسابق"
placeholder="سعر التكلفة السابق"
/>
</FormField>
@ -621,12 +621,12 @@ const ProductsNew = () => {
<FormField
label="السعربالدولار"
label="السعر بالدولار"
>
<Field
type="number"
name="usd_price"
placeholder="السعربالدولار"
placeholder="السعر بالدولار"
/>
</FormField>
@ -657,12 +657,12 @@ const ProductsNew = () => {
<FormField
label="الكميةبالمخزون"
label="الكمية بالمخزون"
>
<Field
type="number"
name="stock_quantity"
placeholder="الكميةبالمخزون"
placeholder="الكمية بالمخزون"
/>
</FormField>
@ -693,12 +693,12 @@ const ProductsNew = () => {
<FormField
label="حدالتنبيهلنقصالمخزون"
label="حد التنبيه لنقص المخزون"
>
<Field
type="number"
name="low_stock_threshold"
placeholder="حدالتنبيهلنقصالمخزون"
placeholder="حد التنبيه لنقص المخزون"
/>
</FormField>
@ -746,7 +746,7 @@ const ProductsNew = () => {
<FormField>
<Field
label='صورالمنتج'
label='صور المنتج'
color='info'
icon={mdiUpload}
path={'products/product_images'}
@ -780,7 +780,7 @@ const ProductsNew = () => {
<FormField label='متاحللبيع' labelFor='is_active'>
<FormField label='متاح للبيع' labelFor='is_active'>
<Field
name='is_active'
id='is_active'
@ -816,7 +816,7 @@ const ProductsNew = () => {
<FormField label="organizations" labelFor="organizations">
<FormField label="المنظمة" labelFor="organizations">
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
</FormField>
@ -828,9 +828,9 @@ const ProductsNew = () => {
<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('/products/products-list')}/>
<BaseButton type="submit" color="info" label="حفظ" />
<BaseButton type="reset" color="info" outline label="إعادة تعيين" />
<BaseButton type='reset' color='danger' outline label='إلغاء' onClick={() => router.push('/products/products-list')}/>
</BaseButtons>
</Form>
</Formik>

Some files were not shown because too many files have changed in this diff Show More