Add/replace Space with my React Vite TypeScript app
Browse files- .gitignore +20 -20
- README.md +1 -81
- eslint.config.js +28 -0
- index.html +13 -0
- op/config.json +3 -0
- op/prompt +5 -0
- package-lock.json +0 -0
- package.json +29 -32
- postcss.config.js +6 -0
- src/App.tsx +237 -0
- src/components/ExcelTable.tsx +319 -0
- src/components/LanguageToggle.tsx +33 -0
- src/components/Navigation.tsx +63 -0
- src/components/ProductCard.tsx +164 -0
- src/components/ProductCatalog.tsx +268 -0
- src/components/ProductForm.tsx +341 -0
- src/components/ProductModal.tsx +184 -0
- src/data/catalogProducts.ts +183 -0
- src/index.css +3 -13
- src/main.tsx +10 -0
- src/types/index.ts +41 -0
- src/utils/excelUtils.ts +198 -0
- src/vite-env.d.ts +1 -0
- tailwind.config.js +8 -0
- tsconfig.app.json +24 -0
- tsconfig.json +7 -0
- tsconfig.node.json +22 -0
- vite.config.ts +10 -0
.gitignore
CHANGED
|
@@ -1,23 +1,23 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
# dependencies
|
| 4 |
-
/node_modules
|
| 5 |
-
/.pnp
|
| 6 |
-
.pnp.js
|
| 7 |
-
|
| 8 |
-
# testing
|
| 9 |
-
/coverage
|
| 10 |
-
|
| 11 |
-
# production
|
| 12 |
-
/build
|
| 13 |
-
|
| 14 |
-
# misc
|
| 15 |
-
.DS_Store
|
| 16 |
-
.env.local
|
| 17 |
-
.env.development.local
|
| 18 |
-
.env.test.local
|
| 19 |
-
.env.production.local
|
| 20 |
-
|
| 21 |
npm-debug.log*
|
| 22 |
yarn-debug.log*
|
| 23 |
yarn-error.log*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
logs
|
| 2 |
+
*.log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
npm-debug.log*
|
| 4 |
yarn-debug.log*
|
| 5 |
yarn-error.log*
|
| 6 |
+
pnpm-debug.log*
|
| 7 |
+
lerna-debug.log*
|
| 8 |
+
|
| 9 |
+
node_modules
|
| 10 |
+
dist
|
| 11 |
+
dist-ssr
|
| 12 |
+
*.local
|
| 13 |
+
|
| 14 |
+
.vscode/*
|
| 15 |
+
!.vscode/extensions.json
|
| 16 |
+
.idea
|
| 17 |
+
.DS_Store
|
| 18 |
+
*.suo
|
| 19 |
+
*.ntvs*
|
| 20 |
+
*.njsproj
|
| 21 |
+
*.sln
|
| 22 |
+
*.sw?
|
| 23 |
+
.env
|
README.md
CHANGED
|
@@ -1,81 +1 @@
|
|
| 1 |
-
|
| 2 |
-
title: Ex12
|
| 3 |
-
emoji: 🐠
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: red
|
| 6 |
-
sdk: static
|
| 7 |
-
pinned: false
|
| 8 |
-
app_build_command: npm run build
|
| 9 |
-
app_file: build/index.html
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
-
# Getting Started with Create React App
|
| 13 |
-
|
| 14 |
-
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
| 15 |
-
|
| 16 |
-
## Available Scripts
|
| 17 |
-
|
| 18 |
-
In the project directory, you can run:
|
| 19 |
-
|
| 20 |
-
### `npm start`
|
| 21 |
-
|
| 22 |
-
Runs the app in the development mode.\
|
| 23 |
-
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
| 24 |
-
|
| 25 |
-
The page will reload when you make changes.\
|
| 26 |
-
You may also see any lint errors in the console.
|
| 27 |
-
|
| 28 |
-
### `npm test`
|
| 29 |
-
|
| 30 |
-
Launches the test runner in the interactive watch mode.\
|
| 31 |
-
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 32 |
-
|
| 33 |
-
### `npm run build`
|
| 34 |
-
|
| 35 |
-
Builds the app for production to the `build` folder.\
|
| 36 |
-
It correctly bundles React in production mode and optimizes the build for the best performance.
|
| 37 |
-
|
| 38 |
-
The build is minified and the filenames include the hashes.\
|
| 39 |
-
Your app is ready to be deployed!
|
| 40 |
-
|
| 41 |
-
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
| 42 |
-
|
| 43 |
-
### `npm run eject`
|
| 44 |
-
|
| 45 |
-
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
| 46 |
-
|
| 47 |
-
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
| 48 |
-
|
| 49 |
-
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
| 50 |
-
|
| 51 |
-
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
| 52 |
-
|
| 53 |
-
## Learn More
|
| 54 |
-
|
| 55 |
-
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
| 56 |
-
|
| 57 |
-
To learn React, check out the [React documentation](https://reactjs.org/).
|
| 58 |
-
|
| 59 |
-
### Code Splitting
|
| 60 |
-
|
| 61 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
| 62 |
-
|
| 63 |
-
### Analyzing the Bundle Size
|
| 64 |
-
|
| 65 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
| 66 |
-
|
| 67 |
-
### Making a Progressive Web App
|
| 68 |
-
|
| 69 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
| 70 |
-
|
| 71 |
-
### Advanced Configuration
|
| 72 |
-
|
| 73 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
| 74 |
-
|
| 75 |
-
### Deployment
|
| 76 |
-
|
| 77 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
| 78 |
-
|
| 79 |
-
### `npm run build` fails to minify
|
| 80 |
-
|
| 81 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
|
|
|
| 1 |
+
app_excel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eslint.config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js';
|
| 2 |
+
import globals from 'globals';
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks';
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
| 5 |
+
import tseslint from 'typescript-eslint';
|
| 6 |
+
|
| 7 |
+
export default tseslint.config(
|
| 8 |
+
{ ignores: ['dist'] },
|
| 9 |
+
{
|
| 10 |
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
languageOptions: {
|
| 13 |
+
ecmaVersion: 2020,
|
| 14 |
+
globals: globals.browser,
|
| 15 |
+
},
|
| 16 |
+
plugins: {
|
| 17 |
+
'react-hooks': reactHooks,
|
| 18 |
+
'react-refresh': reactRefresh,
|
| 19 |
+
},
|
| 20 |
+
rules: {
|
| 21 |
+
...reactHooks.configs.recommended.rules,
|
| 22 |
+
'react-refresh/only-export-components': [
|
| 23 |
+
'warn',
|
| 24 |
+
{ allowConstantExport: true },
|
| 25 |
+
],
|
| 26 |
+
},
|
| 27 |
+
}
|
| 28 |
+
);
|
index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Bilingual Inventory Management Excel App</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
op/config.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"template": "bolt-vite-react-ts"
|
| 3 |
+
}
|
op/prompt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
|
| 2 |
+
|
| 3 |
+
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
|
| 4 |
+
|
| 5 |
+
Use icons from lucide-react for logos.
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -1,39 +1,36 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "react-
|
| 3 |
-
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
-
"
|
| 6 |
-
|
| 7 |
-
"@testing-library/jest-dom": "^6.6.3",
|
| 8 |
-
"@testing-library/react": "^16.3.0",
|
| 9 |
-
"@testing-library/user-event": "^13.5.0",
|
| 10 |
-
"react": "^19.1.0",
|
| 11 |
-
"react-dom": "^19.1.0",
|
| 12 |
-
"react-scripts": "5.0.1",
|
| 13 |
-
"web-vitals": "^2.1.4"
|
| 14 |
-
},
|
| 15 |
"scripts": {
|
| 16 |
-
"
|
| 17 |
-
"build": "
|
| 18 |
-
"
|
| 19 |
-
"
|
|
|
|
| 20 |
},
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
| 26 |
},
|
| 27 |
-
"
|
| 28 |
-
"
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
"
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "vite-react-typescript-starter",
|
|
|
|
| 3 |
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview",
|
| 11 |
+
"typecheck": "tsc --noEmit -p tsconfig.app.json"
|
| 12 |
},
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"@supabase/supabase-js": "^2.57.4",
|
| 15 |
+
"lucide-react": "^0.344.0",
|
| 16 |
+
"react": "^18.3.1",
|
| 17 |
+
"react-dom": "^18.3.1",
|
| 18 |
+
"xlsx": "^0.18.5"
|
| 19 |
},
|
| 20 |
+
"devDependencies": {
|
| 21 |
+
"@eslint/js": "^9.9.1",
|
| 22 |
+
"@types/react": "^18.3.5",
|
| 23 |
+
"@types/react-dom": "^18.3.0",
|
| 24 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 25 |
+
"autoprefixer": "^10.4.18",
|
| 26 |
+
"eslint": "^9.9.1",
|
| 27 |
+
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
| 28 |
+
"eslint-plugin-react-refresh": "^0.4.11",
|
| 29 |
+
"globals": "^15.9.0",
|
| 30 |
+
"postcss": "^8.4.35",
|
| 31 |
+
"tailwindcss": "^3.4.1",
|
| 32 |
+
"typescript": "^5.5.3",
|
| 33 |
+
"typescript-eslint": "^8.3.0",
|
| 34 |
+
"vite": "^5.4.2"
|
| 35 |
}
|
| 36 |
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
};
|
src/App.tsx
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef } from 'react';
|
| 2 |
+
import { Upload, Download, FileSpreadsheet, RefreshCw, Package } from 'lucide-react';
|
| 3 |
+
import { Product, Language, languages } from './types';
|
| 4 |
+
import { readExcelFile, downloadExcelFile, createSampleData } from './utils/excelUtils';
|
| 5 |
+
import { ExcelTable } from './components/ExcelTable';
|
| 6 |
+
import { LanguageToggle } from './components/LanguageToggle';
|
| 7 |
+
import { ProductCatalog } from './components/ProductCatalog';
|
| 8 |
+
import { Navigation } from './components/Navigation';
|
| 9 |
+
|
| 10 |
+
function App() {
|
| 11 |
+
const [products, setProducts] = useState<Product[]>([]);
|
| 12 |
+
const [language, setLanguage] = useState<Language>(languages[0]);
|
| 13 |
+
const [currentPage, setCurrentPage] = useState<'inventory' | 'catalog'>('inventory');
|
| 14 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 15 |
+
const [error, setError] = useState<string>('');
|
| 16 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 17 |
+
|
| 18 |
+
const isArabic = language.code === 'ar';
|
| 19 |
+
|
| 20 |
+
const labels = {
|
| 21 |
+
ar: {
|
| 22 |
+
title: 'إدارة المخزون',
|
| 23 |
+
subtitle: 'نظام إدارة المخزون باستخدام Excel',
|
| 24 |
+
uploadFile: 'تحميل ملف Excel',
|
| 25 |
+
downloadFile: 'تنزيل Excel محدث',
|
| 26 |
+
loadSample: 'تحميل بيانات تجريبية',
|
| 27 |
+
uploadHint: 'اسحب ملف Excel هنا أو انقر للتحديد',
|
| 28 |
+
supportedFormats: 'يدعم ملفات .xlsx',
|
| 29 |
+
loading: 'جارٍ المعالجة...',
|
| 30 |
+
error: 'خطأ',
|
| 31 |
+
success: 'تم بنجاح'
|
| 32 |
+
},
|
| 33 |
+
en: {
|
| 34 |
+
title: 'Inventory Manager',
|
| 35 |
+
subtitle: 'Excel-based Inventory Management System',
|
| 36 |
+
uploadFile: 'Upload Excel File',
|
| 37 |
+
downloadFile: 'Download Updated Excel',
|
| 38 |
+
loadSample: 'Load Sample Data',
|
| 39 |
+
uploadHint: 'Drag Excel file here or click to select',
|
| 40 |
+
supportedFormats: 'Supports .xlsx files',
|
| 41 |
+
loading: 'Processing...',
|
| 42 |
+
error: 'Error',
|
| 43 |
+
success: 'Success'
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
const t = labels[language.code];
|
| 48 |
+
|
| 49 |
+
const handleFileUpload = async (file: File) => {
|
| 50 |
+
if (!file.name.endsWith('.xlsx')) {
|
| 51 |
+
setError(isArabic ? 'يرجى اختيار ملف Excel صالح (.xlsx)' : 'Please select a valid Excel file (.xlsx)');
|
| 52 |
+
return;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
setIsLoading(true);
|
| 56 |
+
setError('');
|
| 57 |
+
|
| 58 |
+
try {
|
| 59 |
+
const parsedProducts = await readExcelFile(file);
|
| 60 |
+
setProducts(parsedProducts);
|
| 61 |
+
} catch (err) {
|
| 62 |
+
setError(isArabic ? 'فشل في قراءة الملف' : 'Failed to read file');
|
| 63 |
+
console.error('File reading error:', err);
|
| 64 |
+
} finally {
|
| 65 |
+
setIsLoading(false);
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
const handleDrop = (e: React.DragEvent) => {
|
| 70 |
+
e.preventDefault();
|
| 71 |
+
const files = Array.from(e.dataTransfer.files);
|
| 72 |
+
if (files.length > 0) {
|
| 73 |
+
handleFileUpload(files[0]);
|
| 74 |
+
}
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
const handleProductUpdate = (updatedProduct: Product) => {
|
| 78 |
+
setProducts(products.map(p => p.id === updatedProduct.id ? updatedProduct : p));
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const handleProductDelete = (productId: string) => {
|
| 82 |
+
setProducts(products.filter(p => p.id !== productId));
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const handleProductAdd = (newProduct: Product) => {
|
| 86 |
+
setProducts([...products, newProduct]);
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
const handleQuantityChange = (productId: string, change: number) => {
|
| 90 |
+
setProducts(products.map(product =>
|
| 91 |
+
product.id === productId
|
| 92 |
+
? { ...product, quantity: Math.max(0, product.quantity + change) }
|
| 93 |
+
: product
|
| 94 |
+
));
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const handleDownloadExcel = () => {
|
| 98 |
+
if (products.length === 0) {
|
| 99 |
+
setError(isArabic ? 'لا توجد بيانات للتنزيل' : 'No data to download');
|
| 100 |
+
return;
|
| 101 |
+
}
|
| 102 |
+
downloadExcelFile(products, 'inventory');
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
const loadSampleData = () => {
|
| 106 |
+
setProducts(createSampleData());
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
return (
|
| 110 |
+
<div className={`min-h-screen bg-gray-50 ${language.dir === 'rtl' ? 'rtl' : 'ltr'}`} dir={language.dir}>
|
| 111 |
+
{/* Header */}
|
| 112 |
+
<header className="bg-white shadow-sm border-b">
|
| 113 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 114 |
+
<div className="flex items-center justify-between h-16">
|
| 115 |
+
<div className="flex items-center gap-4">
|
| 116 |
+
<div className="p-2 bg-blue-100 rounded-lg">
|
| 117 |
+
{currentPage === 'inventory' ? (
|
| 118 |
+
<FileSpreadsheet className="w-8 h-8 text-blue-600" />
|
| 119 |
+
) : (
|
| 120 |
+
<Package className="w-8 h-8 text-blue-600" />
|
| 121 |
+
)}
|
| 122 |
+
</div>
|
| 123 |
+
<div>
|
| 124 |
+
<h1 className="text-2xl font-bold text-gray-900">
|
| 125 |
+
{currentPage === 'inventory' ? t.title : (isArabic ? 'كتالوج المنتجات' : 'Product Catalog')}
|
| 126 |
+
</h1>
|
| 127 |
+
<p className="text-sm text-gray-600">
|
| 128 |
+
{currentPage === 'inventory' ? t.subtitle : (isArabic ? 'تصفح مجموعتنا من المعدات الكهربائية' : 'Browse our electrical equipment collection')}
|
| 129 |
+
</p>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
<LanguageToggle currentLang={language} onLanguageChange={setLanguage} />
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</header>
|
| 136 |
+
|
| 137 |
+
{/* Navigation */}
|
| 138 |
+
<Navigation
|
| 139 |
+
currentPage={currentPage}
|
| 140 |
+
onPageChange={setCurrentPage}
|
| 141 |
+
language={language}
|
| 142 |
+
/>
|
| 143 |
+
|
| 144 |
+
{currentPage === 'catalog' ? (
|
| 145 |
+
<ProductCatalog language={language} />
|
| 146 |
+
) : (
|
| 147 |
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
| 148 |
+
{/* File Upload Area */}
|
| 149 |
+
<div className="mb-8">
|
| 150 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 151 |
+
{/* Upload Section */}
|
| 152 |
+
<div
|
| 153 |
+
onDrop={handleDrop}
|
| 154 |
+
onDragOver={(e) => e.preventDefault()}
|
| 155 |
+
className="md:col-span-2 border-2 border-dashed border-gray-300 rounded-xl p-8 text-center hover:border-blue-400 transition-colors cursor-pointer bg-white"
|
| 156 |
+
onClick={() => fileInputRef.current?.click()}
|
| 157 |
+
>
|
| 158 |
+
<div className="flex flex-col items-center">
|
| 159 |
+
<div className="p-4 bg-blue-50 rounded-full mb-4">
|
| 160 |
+
<Upload className="w-8 h-8 text-blue-500" />
|
| 161 |
+
</div>
|
| 162 |
+
<p className="text-lg font-medium text-gray-700 mb-2">{t.uploadHint}</p>
|
| 163 |
+
<p className="text-sm text-gray-500">{t.supportedFormats}</p>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
{/* Action Buttons */}
|
| 168 |
+
<div className="space-y-3">
|
| 169 |
+
<button
|
| 170 |
+
onClick={handleDownloadExcel}
|
| 171 |
+
disabled={products.length === 0}
|
| 172 |
+
className="w-full bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white px-4 py-3 rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
| 173 |
+
>
|
| 174 |
+
<Download className="w-5 h-5" />
|
| 175 |
+
{t.downloadFile}
|
| 176 |
+
</button>
|
| 177 |
+
|
| 178 |
+
<button
|
| 179 |
+
onClick={loadSampleData}
|
| 180 |
+
className="w-full bg-amber-600 hover:bg-amber-700 text-white px-4 py-3 rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
| 181 |
+
>
|
| 182 |
+
<RefreshCw className="w-5 h-5" />
|
| 183 |
+
{t.loadSample}
|
| 184 |
+
</button>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<input
|
| 189 |
+
ref={fileInputRef}
|
| 190 |
+
type="file"
|
| 191 |
+
accept=".xlsx"
|
| 192 |
+
onChange={(e) => {
|
| 193 |
+
const file = e.target.files?.[0];
|
| 194 |
+
if (file) handleFileUpload(file);
|
| 195 |
+
}}
|
| 196 |
+
className="hidden"
|
| 197 |
+
/>
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
{/* Loading State */}
|
| 201 |
+
{isLoading && (
|
| 202 |
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
| 203 |
+
<div className="flex items-center gap-3">
|
| 204 |
+
<RefreshCw className="w-5 h-5 text-blue-600 animate-spin" />
|
| 205 |
+
<span className="text-blue-700 font-medium">{t.loading}</span>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
)}
|
| 209 |
+
|
| 210 |
+
{/* Error State */}
|
| 211 |
+
{error && (
|
| 212 |
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
| 213 |
+
<div className="flex items-center gap-3">
|
| 214 |
+
<div className="w-5 h-5 bg-red-500 rounded-full flex items-center justify-center">
|
| 215 |
+
<span className="text-white text-xs">!</span>
|
| 216 |
+
</div>
|
| 217 |
+
<span className="text-red-700 font-medium">{error}</span>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
)}
|
| 221 |
+
|
| 222 |
+
{/* Products Table */}
|
| 223 |
+
<ExcelTable
|
| 224 |
+
products={products}
|
| 225 |
+
language={language}
|
| 226 |
+
onProductUpdate={handleProductUpdate}
|
| 227 |
+
onProductDelete={handleProductDelete}
|
| 228 |
+
onProductAdd={handleProductAdd}
|
| 229 |
+
onQuantityChange={handleQuantityChange}
|
| 230 |
+
/>
|
| 231 |
+
</main>
|
| 232 |
+
)}
|
| 233 |
+
</div>
|
| 234 |
+
);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
export default App;
|
src/components/ExcelTable.tsx
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { CreditCard as Edit, Trash2, Minus, Plus, ShoppingCart } from 'lucide-react';
|
| 3 |
+
import { Product, Language } from '../types';
|
| 4 |
+
import { ProductForm } from './ProductForm';
|
| 5 |
+
|
| 6 |
+
interface ExcelTableProps {
|
| 7 |
+
products: Product[];
|
| 8 |
+
language: Language;
|
| 9 |
+
onProductUpdate: (product: Product) => void;
|
| 10 |
+
onProductDelete: (productId: string) => void;
|
| 11 |
+
onProductAdd: (product: Product) => void;
|
| 12 |
+
onQuantityChange: (productId: string, change: number) => void;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export const ExcelTable: React.FC<ExcelTableProps> = ({
|
| 16 |
+
products,
|
| 17 |
+
language,
|
| 18 |
+
onProductUpdate,
|
| 19 |
+
onProductDelete,
|
| 20 |
+
onProductAdd,
|
| 21 |
+
onQuantityChange
|
| 22 |
+
}) => {
|
| 23 |
+
const [editingProduct, setEditingProduct] = useState<Product | undefined>();
|
| 24 |
+
const [showForm, setShowForm] = useState(false);
|
| 25 |
+
const [sellQuantities, setSellQuantities] = useState<{ [key: string]: number }>({});
|
| 26 |
+
|
| 27 |
+
const isArabic = language.code === 'ar';
|
| 28 |
+
|
| 29 |
+
const labels = {
|
| 30 |
+
ar: {
|
| 31 |
+
addProduct: 'إضافة منتج جديد',
|
| 32 |
+
productName: 'اسم المنتج',
|
| 33 |
+
quantity: 'الكمية',
|
| 34 |
+
price: 'السعر',
|
| 35 |
+
category: 'الفئة',
|
| 36 |
+
actions: 'الإجراءات',
|
| 37 |
+
edit: 'تعديل',
|
| 38 |
+
delete: 'حذف',
|
| 39 |
+
sell: 'بيع',
|
| 40 |
+
sellLabel: 'كمية البيع',
|
| 41 |
+
noProducts: 'لا توجد منتجات',
|
| 42 |
+
total: 'المجموع',
|
| 43 |
+
items: 'عنصر',
|
| 44 |
+
value: 'القيمة الإجمالية'
|
| 45 |
+
},
|
| 46 |
+
en: {
|
| 47 |
+
addProduct: 'Add New Product',
|
| 48 |
+
productName: 'Product Name',
|
| 49 |
+
quantity: 'Quantity',
|
| 50 |
+
price: 'Price',
|
| 51 |
+
category: 'Category',
|
| 52 |
+
actions: 'Actions',
|
| 53 |
+
edit: 'Edit',
|
| 54 |
+
delete: 'Delete',
|
| 55 |
+
sell: 'Sell',
|
| 56 |
+
sellLabel: 'Sell Qty',
|
| 57 |
+
noProducts: 'No products found',
|
| 58 |
+
total: 'Total',
|
| 59 |
+
items: 'items',
|
| 60 |
+
value: 'Total Value'
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
const t = labels[language.code];
|
| 65 |
+
|
| 66 |
+
const totalItems = products.reduce((sum, product) => sum + product.quantity, 0);
|
| 67 |
+
const totalValue = products.reduce((sum, product) => sum + (product.quantity * product.price), 0);
|
| 68 |
+
|
| 69 |
+
const handleSellProduct = (productId: string) => {
|
| 70 |
+
const sellQty = sellQuantities[productId] || 1;
|
| 71 |
+
const product = products.find(p => p.id === productId);
|
| 72 |
+
if (product && sellQty <= (product.currentStock || product.quantity)) {
|
| 73 |
+
onQuantityChange(productId, -sellQty);
|
| 74 |
+
// Update sold quantity
|
| 75 |
+
const updatedProduct = {
|
| 76 |
+
...product,
|
| 77 |
+
totalSold: (product.totalSold || 0) + sellQty,
|
| 78 |
+
currentStock: (product.currentStock || product.quantity) - sellQty
|
| 79 |
+
};
|
| 80 |
+
onProductUpdate(updatedProduct);
|
| 81 |
+
setSellQuantities({ ...sellQuantities, [productId]: 1 });
|
| 82 |
+
}
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const formatPrice = (price: number) => {
|
| 86 |
+
return new Intl.NumberFormat(isArabic ? 'ar-SA' : 'en-US', {
|
| 87 |
+
style: 'currency',
|
| 88 |
+
currency: 'SAR'
|
| 89 |
+
}).format(price);
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
return (
|
| 93 |
+
<div className="space-y-6">
|
| 94 |
+
{/* Stats Cards */}
|
| 95 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 96 |
+
<div className="bg-gradient-to-r from-blue-500 to-blue-600 text-white p-6 rounded-xl shadow-lg">
|
| 97 |
+
<div className="flex items-center justify-between">
|
| 98 |
+
<div>
|
| 99 |
+
<p className="text-blue-100 text-sm">{t.total} {t.items}</p>
|
| 100 |
+
<p className="text-3xl font-bold">{totalItems.toLocaleString()}</p>
|
| 101 |
+
</div>
|
| 102 |
+
<ShoppingCart className="w-10 h-10 text-blue-200" />
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div className="bg-gradient-to-r from-emerald-500 to-emerald-600 text-white p-6 rounded-xl shadow-lg">
|
| 107 |
+
<div className="flex items-center justify-between">
|
| 108 |
+
<div>
|
| 109 |
+
<p className="text-emerald-100 text-sm">{t.value}</p>
|
| 110 |
+
<p className="text-3xl font-bold">{formatPrice(totalValue)}</p>
|
| 111 |
+
</div>
|
| 112 |
+
<div className="w-10 h-10 bg-emerald-400 rounded-full flex items-center justify-center">
|
| 113 |
+
<span className="text-2xl">💰</span>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<div className="bg-gradient-to-r from-amber-500 to-amber-600 text-white p-6 rounded-xl shadow-lg">
|
| 119 |
+
<div className="flex items-center justify-between">
|
| 120 |
+
<div>
|
| 121 |
+
<p className="text-amber-100 text-sm">Products</p>
|
| 122 |
+
<p className="text-3xl font-bold">{products.length}</p>
|
| 123 |
+
</div>
|
| 124 |
+
<div className="w-10 h-10 bg-amber-400 rounded-full flex items-center justify-center">
|
| 125 |
+
<span className="text-2xl">📦</span>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
{/* Add Product Button */}
|
| 132 |
+
<div className="flex justify-between items-center">
|
| 133 |
+
<h2 className="text-2xl font-bold text-gray-900">
|
| 134 |
+
{isArabic ? 'إدارة المخزون' : 'Inventory Management'}
|
| 135 |
+
</h2>
|
| 136 |
+
<button
|
| 137 |
+
onClick={() => setShowForm(true)}
|
| 138 |
+
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center gap-2 shadow-lg"
|
| 139 |
+
>
|
| 140 |
+
<Plus className="w-5 h-5" />
|
| 141 |
+
{t.addProduct}
|
| 142 |
+
</button>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
{/* Table */}
|
| 146 |
+
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
|
| 147 |
+
{products.length === 0 ? (
|
| 148 |
+
<div className="p-12 text-center">
|
| 149 |
+
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 150 |
+
<ShoppingCart className="w-12 h-12 text-gray-400" />
|
| 151 |
+
</div>
|
| 152 |
+
<p className="text-xl text-gray-500 mb-2">{t.noProducts}</p>
|
| 153 |
+
<p className="text-gray-400 mb-6">
|
| 154 |
+
{isArabic ? 'قم بإضافة منتج جديد أو تحميل ملف Excel' : 'Add a new product or upload an Excel file'}
|
| 155 |
+
</p>
|
| 156 |
+
</div>
|
| 157 |
+
) : (
|
| 158 |
+
<div className="overflow-x-auto">
|
| 159 |
+
<table className="w-full">
|
| 160 |
+
<thead className="bg-gray-50 border-b border-gray-200">
|
| 161 |
+
<tr>
|
| 162 |
+
<th className="text-left py-4 px-6 font-semibold text-gray-700">
|
| 163 |
+
{t.supplier}
|
| 164 |
+
</th>
|
| 165 |
+
<th className="text-left py-4 px-6 font-semibold text-gray-700">
|
| 166 |
+
{t.productName}
|
| 167 |
+
</th>
|
| 168 |
+
<th className="text-left py-4 px-6 font-semibold text-gray-700">
|
| 169 |
+
{t.received}
|
| 170 |
+
</th>
|
| 171 |
+
<th className="text-left py-4 px-6 font-semibold text-gray-700">
|
| 172 |
+
{t.sold}
|
| 173 |
+
</th>
|
| 174 |
+
<th className="text-left py-4 px-6 font-semibold text-gray-700">
|
| 175 |
+
{t.remaining}
|
| 176 |
+
</th>
|
| 177 |
+
<th className="text-left py-4 px-6 font-semibold text-gray-700">
|
| 178 |
+
{t.price}
|
| 179 |
+
</th>
|
| 180 |
+
<th className="text-left py-4 px-6 font-semibold text-gray-700">
|
| 181 |
+
{t.sell}
|
| 182 |
+
</th>
|
| 183 |
+
<th className="text-right py-4 px-6 font-semibold text-gray-700">
|
| 184 |
+
{t.actions}
|
| 185 |
+
</th>
|
| 186 |
+
</tr>
|
| 187 |
+
</thead>
|
| 188 |
+
<tbody className="divide-y divide-gray-200">
|
| 189 |
+
{products.map((product) => (
|
| 190 |
+
<tr key={product.id} className="hover:bg-gray-50 transition-colors">
|
| 191 |
+
<td className="py-4 px-6">
|
| 192 |
+
<div className="text-sm">
|
| 193 |
+
<div className="font-medium text-gray-900">
|
| 194 |
+
{isArabic ? product.supplierAr : product.supplier}
|
| 195 |
+
</div>
|
| 196 |
+
<div className="text-gray-500 text-xs">
|
| 197 |
+
{product.importDate}
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</td>
|
| 201 |
+
<td className="py-4 px-6">
|
| 202 |
+
<div>
|
| 203 |
+
<div className="font-medium text-gray-900">
|
| 204 |
+
{isArabic ? product.nameAr : product.nameEn}
|
| 205 |
+
</div>
|
| 206 |
+
<div className="text-sm text-gray-500">
|
| 207 |
+
{product.secoCode && `Code: ${product.secoCode}`}
|
| 208 |
+
{product.categoryAr && (
|
| 209 |
+
<div className="text-xs text-gray-400">
|
| 210 |
+
{isArabic ? product.categoryAr : product.categoryEn}
|
| 211 |
+
</div>
|
| 212 |
+
)}
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
</td>
|
| 216 |
+
<td className="py-4 px-6">
|
| 217 |
+
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
| 218 |
+
{product.totalReceived || 0}
|
| 219 |
+
</span>
|
| 220 |
+
</td>
|
| 221 |
+
<td className="py-4 px-6 font-medium">
|
| 222 |
+
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
|
| 223 |
+
{product.totalSold || 0}
|
| 224 |
+
</span>
|
| 225 |
+
</td>
|
| 226 |
+
<td className="py-4 px-6 text-gray-600">
|
| 227 |
+
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
| 228 |
+
(product.currentStock || product.quantity) > 10
|
| 229 |
+
? 'bg-green-100 text-green-800'
|
| 230 |
+
: (product.currentStock || product.quantity) > 0
|
| 231 |
+
? 'bg-yellow-100 text-yellow-800'
|
| 232 |
+
: 'bg-red-100 text-red-800'
|
| 233 |
+
}`}>
|
| 234 |
+
{product.currentStock || product.quantity}
|
| 235 |
+
</span>
|
| 236 |
+
</td>
|
| 237 |
+
<td className="py-4 px-6 font-medium">
|
| 238 |
+
{formatPrice(product.price)}
|
| 239 |
+
</td>
|
| 240 |
+
<td className="py-4 px-6">
|
| 241 |
+
<div className="flex items-center gap-2">
|
| 242 |
+
<input
|
| 243 |
+
type="number"
|
| 244 |
+
min="1"
|
| 245 |
+
max={product.currentStock || product.quantity}
|
| 246 |
+
value={sellQuantities[product.id] || 1}
|
| 247 |
+
onChange={(e) => setSellQuantities({
|
| 248 |
+
...sellQuantities,
|
| 249 |
+
[product.id]: Math.min(Number(e.target.value), product.currentStock || product.quantity)
|
| 250 |
+
})}
|
| 251 |
+
className="w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 252 |
+
/>
|
| 253 |
+
<button
|
| 254 |
+
onClick={() => handleSellProduct(product.id)}
|
| 255 |
+
disabled={(product.currentStock || product.quantity) === 0}
|
| 256 |
+
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded transition-colors disabled:text-gray-400 disabled:hover:bg-transparent"
|
| 257 |
+
title={t.sell}
|
| 258 |
+
>
|
| 259 |
+
<Minus className="w-4 h-4" />
|
| 260 |
+
</button>
|
| 261 |
+
</div>
|
| 262 |
+
</td>
|
| 263 |
+
<td className="py-4 px-6">
|
| 264 |
+
<div className="flex items-center justify-end gap-2">
|
| 265 |
+
<button
|
| 266 |
+
onClick={() => onQuantityChange(product.id, 1)}
|
| 267 |
+
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
| 268 |
+
title="Add 1"
|
| 269 |
+
>
|
| 270 |
+
<Plus className="w-4 h-4" />
|
| 271 |
+
</button>
|
| 272 |
+
<button
|
| 273 |
+
onClick={() => {
|
| 274 |
+
setEditingProduct(product);
|
| 275 |
+
setShowForm(true);
|
| 276 |
+
}}
|
| 277 |
+
className="p-2 text-amber-600 hover:bg-amber-50 rounded-lg transition-colors"
|
| 278 |
+
title={t.edit}
|
| 279 |
+
>
|
| 280 |
+
<Edit className="w-4 h-4" />
|
| 281 |
+
</button>
|
| 282 |
+
<button
|
| 283 |
+
onClick={() => onProductDelete(product.id)}
|
| 284 |
+
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
| 285 |
+
title={t.delete}
|
| 286 |
+
>
|
| 287 |
+
<Trash2 className="w-4 h-4" />
|
| 288 |
+
</button>
|
| 289 |
+
</div>
|
| 290 |
+
</td>
|
| 291 |
+
</tr>
|
| 292 |
+
))}
|
| 293 |
+
</tbody>
|
| 294 |
+
</table>
|
| 295 |
+
</div>
|
| 296 |
+
)}
|
| 297 |
+
</div>
|
| 298 |
+
|
| 299 |
+
{/* Product Form Modal */}
|
| 300 |
+
<ProductForm
|
| 301 |
+
product={editingProduct}
|
| 302 |
+
isOpen={showForm}
|
| 303 |
+
onClose={() => {
|
| 304 |
+
setShowForm(false);
|
| 305 |
+
setEditingProduct(undefined);
|
| 306 |
+
}}
|
| 307 |
+
onSave={(product) => {
|
| 308 |
+
if (editingProduct) {
|
| 309 |
+
onProductUpdate(product);
|
| 310 |
+
} else {
|
| 311 |
+
onProductAdd(product);
|
| 312 |
+
}
|
| 313 |
+
setEditingProduct(undefined);
|
| 314 |
+
}}
|
| 315 |
+
language={language}
|
| 316 |
+
/>
|
| 317 |
+
</div>
|
| 318 |
+
);
|
| 319 |
+
};
|
src/components/LanguageToggle.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Globe } from 'lucide-react';
|
| 3 |
+
import { Language, languages } from '../types';
|
| 4 |
+
|
| 5 |
+
interface LanguageToggleProps {
|
| 6 |
+
currentLang: Language;
|
| 7 |
+
onLanguageChange: (lang: Language) => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export const LanguageToggle: React.FC<LanguageToggleProps> = ({
|
| 11 |
+
currentLang,
|
| 12 |
+
onLanguageChange
|
| 13 |
+
}) => {
|
| 14 |
+
return (
|
| 15 |
+
<div className="flex items-center gap-2 bg-white rounded-lg shadow-sm border p-2">
|
| 16 |
+
<Globe className="w-4 h-4 text-gray-600" />
|
| 17 |
+
<select
|
| 18 |
+
value={currentLang.code}
|
| 19 |
+
onChange={(e) => {
|
| 20 |
+
const lang = languages.find(l => l.code === e.target.value);
|
| 21 |
+
if (lang) onLanguageChange(lang);
|
| 22 |
+
}}
|
| 23 |
+
className="border-none outline-none bg-transparent text-sm font-medium"
|
| 24 |
+
>
|
| 25 |
+
{languages.map(lang => (
|
| 26 |
+
<option key={lang.code} value={lang.code}>
|
| 27 |
+
{lang.name}
|
| 28 |
+
</option>
|
| 29 |
+
))}
|
| 30 |
+
</select>
|
| 31 |
+
</div>
|
| 32 |
+
);
|
| 33 |
+
};
|
src/components/Navigation.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { FileSpreadsheet, Package } from 'lucide-react';
|
| 3 |
+
import { Language } from '../types';
|
| 4 |
+
|
| 5 |
+
interface NavigationProps {
|
| 6 |
+
currentPage: 'inventory' | 'catalog';
|
| 7 |
+
onPageChange: (page: 'inventory' | 'catalog') => void;
|
| 8 |
+
language: Language;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export const Navigation: React.FC<NavigationProps> = ({
|
| 12 |
+
currentPage,
|
| 13 |
+
onPageChange,
|
| 14 |
+
language
|
| 15 |
+
}) => {
|
| 16 |
+
const isArabic = language.code === 'ar';
|
| 17 |
+
|
| 18 |
+
const labels = {
|
| 19 |
+
ar: {
|
| 20 |
+
inventory: 'إدارة المخزون',
|
| 21 |
+
catalog: 'كتالوج المنتجات'
|
| 22 |
+
},
|
| 23 |
+
en: {
|
| 24 |
+
inventory: 'Inventory Management',
|
| 25 |
+
catalog: 'Product Catalog'
|
| 26 |
+
}
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const t = labels[language.code];
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<nav className="bg-white border-b border-gray-200">
|
| 33 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 34 |
+
<div className="flex justify-center">
|
| 35 |
+
<div className="flex space-x-8">
|
| 36 |
+
<button
|
| 37 |
+
onClick={() => onPageChange('inventory')}
|
| 38 |
+
className={`flex items-center gap-2 py-4 px-6 border-b-2 font-medium text-sm transition-colors ${
|
| 39 |
+
currentPage === 'inventory'
|
| 40 |
+
? 'border-blue-500 text-blue-600'
|
| 41 |
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
| 42 |
+
}`}
|
| 43 |
+
>
|
| 44 |
+
<FileSpreadsheet className="w-5 h-5" />
|
| 45 |
+
{t.inventory}
|
| 46 |
+
</button>
|
| 47 |
+
<button
|
| 48 |
+
onClick={() => onPageChange('catalog')}
|
| 49 |
+
className={`flex items-center gap-2 py-4 px-6 border-b-2 font-medium text-sm transition-colors ${
|
| 50 |
+
currentPage === 'catalog'
|
| 51 |
+
? 'border-blue-500 text-blue-600'
|
| 52 |
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
| 53 |
+
}`}
|
| 54 |
+
>
|
| 55 |
+
<Package className="w-5 h-5" />
|
| 56 |
+
{t.catalog}
|
| 57 |
+
</button>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</nav>
|
| 62 |
+
);
|
| 63 |
+
};
|
src/components/ProductCard.tsx
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Package, Phone, Mail, CheckCircle, XCircle } from 'lucide-react';
|
| 3 |
+
import { CatalogProduct, Language } from '../types';
|
| 4 |
+
|
| 5 |
+
interface ProductCardProps {
|
| 6 |
+
product: CatalogProduct;
|
| 7 |
+
language: Language;
|
| 8 |
+
viewMode: 'grid' | 'list';
|
| 9 |
+
onProductClick: (product: CatalogProduct) => void;
|
| 10 |
+
onWhatsAppContact: (product: CatalogProduct) => void;
|
| 11 |
+
onEmailContact: (product: CatalogProduct) => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const ProductCard: React.FC<ProductCardProps> = ({
|
| 15 |
+
product,
|
| 16 |
+
language,
|
| 17 |
+
viewMode,
|
| 18 |
+
onProductClick,
|
| 19 |
+
onWhatsAppContact,
|
| 20 |
+
onEmailContact
|
| 21 |
+
}) => {
|
| 22 |
+
const isArabic = language.code === 'ar';
|
| 23 |
+
|
| 24 |
+
const labels = {
|
| 25 |
+
ar: {
|
| 26 |
+
inStock: 'متوفر',
|
| 27 |
+
outOfStock: 'غير متوفر',
|
| 28 |
+
viewDetails: 'عرض التفاصيل',
|
| 29 |
+
whatsapp: 'واتساب',
|
| 30 |
+
email: 'إيميل',
|
| 31 |
+
category: 'الفئة'
|
| 32 |
+
},
|
| 33 |
+
en: {
|
| 34 |
+
inStock: 'In Stock',
|
| 35 |
+
outOfStock: 'Out of Stock',
|
| 36 |
+
viewDetails: 'View Details',
|
| 37 |
+
whatsapp: 'WhatsApp',
|
| 38 |
+
email: 'Email',
|
| 39 |
+
category: 'Category'
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
const t = labels[language.code];
|
| 44 |
+
|
| 45 |
+
if (viewMode === 'list') {
|
| 46 |
+
return (
|
| 47 |
+
<div className="bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow p-6">
|
| 48 |
+
<div className="flex items-start gap-6">
|
| 49 |
+
<div className="flex-shrink-0">
|
| 50 |
+
<div className="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center">
|
| 51 |
+
<Package className="w-10 h-10 text-gray-400" />
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<div className="flex-1 min-w-0">
|
| 56 |
+
<div className="flex items-start justify-between mb-2">
|
| 57 |
+
<h3 className="text-lg font-semibold text-gray-900 line-clamp-2">
|
| 58 |
+
{product.name}
|
| 59 |
+
</h3>
|
| 60 |
+
<div className="flex items-center gap-2 ml-4">
|
| 61 |
+
{product.inStock ? (
|
| 62 |
+
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
| 63 |
+
<CheckCircle className="w-4 h-4 mr-1" />
|
| 64 |
+
{t.inStock}
|
| 65 |
+
</span>
|
| 66 |
+
) : (
|
| 67 |
+
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
|
| 68 |
+
<XCircle className="w-4 h-4 mr-1" />
|
| 69 |
+
{t.outOfStock}
|
| 70 |
+
</span>
|
| 71 |
+
)}
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<p className="text-sm text-blue-600 font-medium mb-2">{product.category}</p>
|
| 76 |
+
<p className="text-gray-600 line-clamp-2 mb-4">{product.description}</p>
|
| 77 |
+
|
| 78 |
+
<div className="flex items-center gap-3">
|
| 79 |
+
<button
|
| 80 |
+
onClick={() => onProductClick(product)}
|
| 81 |
+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
| 82 |
+
>
|
| 83 |
+
{t.viewDetails}
|
| 84 |
+
</button>
|
| 85 |
+
<button
|
| 86 |
+
onClick={() => onWhatsAppContact(product)}
|
| 87 |
+
className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors font-medium flex items-center gap-2"
|
| 88 |
+
>
|
| 89 |
+
<Phone className="w-4 h-4" />
|
| 90 |
+
{t.whatsapp}
|
| 91 |
+
</button>
|
| 92 |
+
<button
|
| 93 |
+
onClick={() => onEmailContact(product)}
|
| 94 |
+
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors font-medium flex items-center gap-2"
|
| 95 |
+
>
|
| 96 |
+
<Mail className="w-4 h-4" />
|
| 97 |
+
{t.email}
|
| 98 |
+
</button>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return (
|
| 107 |
+
<div className="bg-white rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 overflow-hidden group">
|
| 108 |
+
<div className="aspect-square bg-gray-100 flex items-center justify-center p-8">
|
| 109 |
+
<Package className="w-16 h-16 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div className="p-6">
|
| 113 |
+
<div className="flex items-center justify-between mb-2">
|
| 114 |
+
<span className="text-sm text-blue-600 font-medium">{product.category}</span>
|
| 115 |
+
{product.inStock ? (
|
| 116 |
+
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
| 117 |
+
<CheckCircle className="w-3 h-3 mr-1" />
|
| 118 |
+
{t.inStock}
|
| 119 |
+
</span>
|
| 120 |
+
) : (
|
| 121 |
+
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
| 122 |
+
<XCircle className="w-3 h-3 mr-1" />
|
| 123 |
+
{t.outOfStock}
|
| 124 |
+
</span>
|
| 125 |
+
)}
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<h3 className="font-semibold text-gray-900 mb-2 line-clamp-2 min-h-[3rem]">
|
| 129 |
+
{product.name}
|
| 130 |
+
</h3>
|
| 131 |
+
|
| 132 |
+
<p className="text-sm text-gray-600 line-clamp-3 mb-4 min-h-[4rem]">
|
| 133 |
+
{product.description}
|
| 134 |
+
</p>
|
| 135 |
+
|
| 136 |
+
<div className="space-y-2">
|
| 137 |
+
<button
|
| 138 |
+
onClick={() => onProductClick(product)}
|
| 139 |
+
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
| 140 |
+
>
|
| 141 |
+
{t.viewDetails}
|
| 142 |
+
</button>
|
| 143 |
+
|
| 144 |
+
<div className="grid grid-cols-2 gap-2">
|
| 145 |
+
<button
|
| 146 |
+
onClick={() => onWhatsAppContact(product)}
|
| 147 |
+
className="px-3 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors font-medium flex items-center justify-center gap-1 text-sm"
|
| 148 |
+
>
|
| 149 |
+
<Phone className="w-4 h-4" />
|
| 150 |
+
{t.whatsapp}
|
| 151 |
+
</button>
|
| 152 |
+
<button
|
| 153 |
+
onClick={() => onEmailContact(product)}
|
| 154 |
+
className="px-3 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors font-medium flex items-center justify-center gap-1 text-sm"
|
| 155 |
+
>
|
| 156 |
+
<Mail className="w-4 h-4" />
|
| 157 |
+
{t.email}
|
| 158 |
+
</button>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
);
|
| 164 |
+
};
|
src/components/ProductCatalog.tsx
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Search, Filter, ShoppingCart, Phone, Mail, Package, Grid2x2 as Grid, List } from 'lucide-react';
|
| 3 |
+
import { Language, CatalogProduct } from '../types';
|
| 4 |
+
import { catalogProducts, productCategories } from '../data/catalogProducts';
|
| 5 |
+
import { ProductCard } from './ProductCard';
|
| 6 |
+
import { ProductModal } from './ProductModal';
|
| 7 |
+
|
| 8 |
+
interface ProductCatalogProps {
|
| 9 |
+
language: Language;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const ProductCatalog: React.FC<ProductCatalogProps> = ({ language }) => {
|
| 13 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 14 |
+
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
| 15 |
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
| 16 |
+
const [selectedProduct, setSelectedProduct] = useState<CatalogProduct | null>(null);
|
| 17 |
+
const [showModal, setShowModal] = useState(false);
|
| 18 |
+
|
| 19 |
+
const isArabic = language.code === 'ar';
|
| 20 |
+
|
| 21 |
+
const labels = {
|
| 22 |
+
ar: {
|
| 23 |
+
title: 'كتالوج المنتجات',
|
| 24 |
+
subtitle: 'تصفح مجموعتنا الواسعة من المعدات الكهربائية',
|
| 25 |
+
search: 'البحث في المنتجات...',
|
| 26 |
+
category: 'الفئة',
|
| 27 |
+
allCategories: 'جميع الفئات',
|
| 28 |
+
viewGrid: 'عرض شبكي',
|
| 29 |
+
viewList: 'عرض قائمة',
|
| 30 |
+
productsFound: 'منتج موجود',
|
| 31 |
+
noProducts: 'لم يتم العثور على منتجات',
|
| 32 |
+
noProductsDesc: 'جرب تغيير مصطلحات البحث أو الفئة',
|
| 33 |
+
contactInfo: 'معلومات الاتصال للشراء',
|
| 34 |
+
phone: 'الهاتف',
|
| 35 |
+
email: 'البريد الإلكتروني',
|
| 36 |
+
whatsapp: 'واتساب',
|
| 37 |
+
sendMessage: 'إرسال رسالة',
|
| 38 |
+
inStock: 'متوفر',
|
| 39 |
+
outOfStock: 'غير متوفر',
|
| 40 |
+
specifications: 'المواصفات',
|
| 41 |
+
description: 'الوصف'
|
| 42 |
+
},
|
| 43 |
+
en: {
|
| 44 |
+
title: 'Product Catalog',
|
| 45 |
+
subtitle: 'Browse our extensive collection of electrical equipment',
|
| 46 |
+
search: 'Search products...',
|
| 47 |
+
category: 'Category',
|
| 48 |
+
allCategories: 'All Categories',
|
| 49 |
+
viewGrid: 'Grid View',
|
| 50 |
+
viewList: 'List View',
|
| 51 |
+
productsFound: 'products found',
|
| 52 |
+
noProducts: 'No products found',
|
| 53 |
+
noProductsDesc: 'Try changing your search terms or category',
|
| 54 |
+
contactInfo: 'Contact Information for Purchase',
|
| 55 |
+
phone: 'Phone',
|
| 56 |
+
email: 'Email',
|
| 57 |
+
whatsapp: 'WhatsApp',
|
| 58 |
+
sendMessage: 'Send Message',
|
| 59 |
+
inStock: 'In Stock',
|
| 60 |
+
outOfStock: 'Out of Stock',
|
| 61 |
+
specifications: 'Specifications',
|
| 62 |
+
description: 'Description'
|
| 63 |
+
}
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
const t = labels[language.code];
|
| 67 |
+
|
| 68 |
+
// Filter products based on search and category
|
| 69 |
+
const filteredProducts = catalogProducts.filter(product => {
|
| 70 |
+
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 71 |
+
product.description.toLowerCase().includes(searchTerm.toLowerCase());
|
| 72 |
+
const matchesCategory = selectedCategory === 'All Categories' || product.category === selectedCategory;
|
| 73 |
+
return matchesSearch && matchesCategory;
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
const handleProductClick = (product: CatalogProduct) => {
|
| 77 |
+
setSelectedProduct(product);
|
| 78 |
+
setShowModal(true);
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const handleWhatsAppContact = (product?: CatalogProduct) => {
|
| 82 |
+
const phoneNumber = '1872752725';
|
| 83 |
+
const message = product
|
| 84 |
+
? `Hello, I'm interested in purchasing: ${product.name}. Please provide more details.`
|
| 85 |
+
: 'Hello, I would like to inquire about your electrical equipment products.';
|
| 86 |
+
|
| 87 |
+
const whatsappUrl = `https://wa.me/${phoneNumber}?text=${encodeURIComponent(message)}`;
|
| 88 |
+
window.open(whatsappUrl, '_blank');
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
const handleEmailContact = (product?: CatalogProduct) => {
|
| 92 |
+
const subject = product
|
| 93 |
+
? `Inquiry about ${product.name}`
|
| 94 |
+
: 'Product Inquiry';
|
| 95 |
+
const body = product
|
| 96 |
+
? `Hello,\n\nI'm interested in purchasing the following product:\n\nProduct: ${product.name}\nDescription: ${product.description}\n\nPlease provide pricing and availability information.\n\nThank you.`
|
| 97 |
+
: 'Hello,\n\nI would like to inquire about your electrical equipment products.\n\nThank you.';
|
| 98 |
+
|
| 99 |
+
const mailtoUrl = `mailto:saif@gmail.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
| 100 |
+
window.location.href = mailtoUrl;
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
<div className={`min-h-screen bg-gray-50 ${language.dir === 'rtl' ? 'rtl' : 'ltr'}`} dir={language.dir}>
|
| 105 |
+
{/* Header */}
|
| 106 |
+
<div className="bg-white shadow-sm border-b">
|
| 107 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
| 108 |
+
<div className="text-center">
|
| 109 |
+
<div className="flex items-center justify-center gap-3 mb-4">
|
| 110 |
+
<div className="p-3 bg-blue-100 rounded-xl">
|
| 111 |
+
<Package className="w-8 h-8 text-blue-600" />
|
| 112 |
+
</div>
|
| 113 |
+
<h1 className="text-4xl font-bold text-gray-900">{t.title}</h1>
|
| 114 |
+
</div>
|
| 115 |
+
<p className="text-xl text-gray-600 max-w-2xl mx-auto">{t.subtitle}</p>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
| 121 |
+
{/* Contact Information Card */}
|
| 122 |
+
<div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl p-6 mb-8 shadow-lg">
|
| 123 |
+
<h2 className="text-2xl font-bold mb-4 flex items-center gap-3">
|
| 124 |
+
<ShoppingCart className="w-6 h-6" />
|
| 125 |
+
{t.contactInfo}
|
| 126 |
+
</h2>
|
| 127 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 128 |
+
<div className="flex items-center gap-3">
|
| 129 |
+
<Phone className="w-5 h-5 text-blue-200" />
|
| 130 |
+
<div>
|
| 131 |
+
<p className="font-medium">{t.phone}</p>
|
| 132 |
+
<p className="text-blue-100">+1872752725</p>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
<div className="flex items-center gap-3">
|
| 136 |
+
<Mail className="w-5 h-5 text-blue-200" />
|
| 137 |
+
<div>
|
| 138 |
+
<p className="font-medium">{t.email}</p>
|
| 139 |
+
<p className="text-blue-100">saif@gmail.com</p>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
<div className="flex gap-3">
|
| 143 |
+
<button
|
| 144 |
+
onClick={() => handleWhatsAppContact()}
|
| 145 |
+
className="flex-1 bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
| 146 |
+
>
|
| 147 |
+
<Phone className="w-4 h-4" />
|
| 148 |
+
{t.whatsapp}
|
| 149 |
+
</button>
|
| 150 |
+
<button
|
| 151 |
+
onClick={() => handleEmailContact()}
|
| 152 |
+
className="flex-1 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
| 153 |
+
>
|
| 154 |
+
<Mail className="w-4 h-4" />
|
| 155 |
+
{t.sendMessage}
|
| 156 |
+
</button>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
{/* Search and Filter Controls */}
|
| 162 |
+
<div className="bg-white rounded-xl shadow-sm p-6 mb-8">
|
| 163 |
+
<div className="flex flex-col lg:flex-row gap-4 items-center justify-between">
|
| 164 |
+
<div className="flex-1 w-full lg:max-w-md">
|
| 165 |
+
<div className="relative">
|
| 166 |
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
| 167 |
+
<input
|
| 168 |
+
type="text"
|
| 169 |
+
placeholder={t.search}
|
| 170 |
+
value={searchTerm}
|
| 171 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 172 |
+
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 173 |
+
/>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<div className="flex items-center gap-4">
|
| 178 |
+
<div className="flex items-center gap-2">
|
| 179 |
+
<Filter className="w-5 h-5 text-gray-500" />
|
| 180 |
+
<select
|
| 181 |
+
value={selectedCategory}
|
| 182 |
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
| 183 |
+
className="border border-gray-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 184 |
+
>
|
| 185 |
+
{productCategories.map(category => (
|
| 186 |
+
<option key={category} value={category}>
|
| 187 |
+
{category === 'All Categories' ? t.allCategories : category}
|
| 188 |
+
</option>
|
| 189 |
+
))}
|
| 190 |
+
</select>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<div className="flex items-center bg-gray-100 rounded-lg p-1">
|
| 194 |
+
<button
|
| 195 |
+
onClick={() => setViewMode('grid')}
|
| 196 |
+
className={`p-2 rounded-md transition-colors ${
|
| 197 |
+
viewMode === 'grid' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
|
| 198 |
+
}`}
|
| 199 |
+
title={t.viewGrid}
|
| 200 |
+
>
|
| 201 |
+
<Grid className="w-5 h-5" />
|
| 202 |
+
</button>
|
| 203 |
+
<button
|
| 204 |
+
onClick={() => setViewMode('list')}
|
| 205 |
+
className={`p-2 rounded-md transition-colors ${
|
| 206 |
+
viewMode === 'list' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
|
| 207 |
+
}`}
|
| 208 |
+
title={t.viewList}
|
| 209 |
+
>
|
| 210 |
+
<List className="w-5 h-5" />
|
| 211 |
+
</button>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
{/* Results Count */}
|
| 217 |
+
<div className="mt-4 pt-4 border-t border-gray-200">
|
| 218 |
+
<p className="text-gray-600">
|
| 219 |
+
<span className="font-semibold text-blue-600">{filteredProducts.length}</span> {t.productsFound}
|
| 220 |
+
</p>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
{/* Products Grid/List */}
|
| 225 |
+
{filteredProducts.length === 0 ? (
|
| 226 |
+
<div className="bg-white rounded-xl shadow-sm p-12 text-center">
|
| 227 |
+
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 228 |
+
<Package className="w-12 h-12 text-gray-400" />
|
| 229 |
+
</div>
|
| 230 |
+
<h3 className="text-xl font-semibold text-gray-700 mb-2">{t.noProducts}</h3>
|
| 231 |
+
<p className="text-gray-500">{t.noProductsDesc}</p>
|
| 232 |
+
</div>
|
| 233 |
+
) : (
|
| 234 |
+
<div className={`${
|
| 235 |
+
viewMode === 'grid'
|
| 236 |
+
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6'
|
| 237 |
+
: 'space-y-4'
|
| 238 |
+
}`}>
|
| 239 |
+
{filteredProducts.map(product => (
|
| 240 |
+
<ProductCard
|
| 241 |
+
key={product.id}
|
| 242 |
+
product={product}
|
| 243 |
+
language={language}
|
| 244 |
+
viewMode={viewMode}
|
| 245 |
+
onProductClick={handleProductClick}
|
| 246 |
+
onWhatsAppContact={handleWhatsAppContact}
|
| 247 |
+
onEmailContact={handleEmailContact}
|
| 248 |
+
/>
|
| 249 |
+
))}
|
| 250 |
+
</div>
|
| 251 |
+
)}
|
| 252 |
+
</div>
|
| 253 |
+
|
| 254 |
+
{/* Product Modal */}
|
| 255 |
+
<ProductModal
|
| 256 |
+
product={selectedProduct}
|
| 257 |
+
isOpen={showModal}
|
| 258 |
+
onClose={() => {
|
| 259 |
+
setShowModal(false);
|
| 260 |
+
setSelectedProduct(null);
|
| 261 |
+
}}
|
| 262 |
+
language={language}
|
| 263 |
+
onWhatsAppContact={handleWhatsAppContact}
|
| 264 |
+
onEmailContact={handleEmailContact}
|
| 265 |
+
/>
|
| 266 |
+
</div>
|
| 267 |
+
);
|
| 268 |
+
};
|
src/components/ProductForm.tsx
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { X, Save } from 'lucide-react';
|
| 3 |
+
import { Product, Language } from '../types';
|
| 4 |
+
|
| 5 |
+
interface ProductFormProps {
|
| 6 |
+
product?: Product;
|
| 7 |
+
isOpen: boolean;
|
| 8 |
+
onClose: () => void;
|
| 9 |
+
onSave: (product: Product) => void;
|
| 10 |
+
language: Language;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export const ProductForm: React.FC<ProductFormProps> = ({
|
| 14 |
+
product,
|
| 15 |
+
isOpen,
|
| 16 |
+
onClose,
|
| 17 |
+
onSave,
|
| 18 |
+
language
|
| 19 |
+
}) => {
|
| 20 |
+
const [formData, setFormData] = useState<Omit<Product, 'id'>>({
|
| 21 |
+
nameAr: '',
|
| 22 |
+
nameEn: '',
|
| 23 |
+
quantity: 0,
|
| 24 |
+
price: 0,
|
| 25 |
+
categoryAr: '',
|
| 26 |
+
categoryEn: '',
|
| 27 |
+
description: '',
|
| 28 |
+
secoCode: '',
|
| 29 |
+
supplier: '',
|
| 30 |
+
supplierAr: '',
|
| 31 |
+
importDate: new Date().toISOString().split('T')[0],
|
| 32 |
+
inventoryDates: {},
|
| 33 |
+
totalReceived: 0,
|
| 34 |
+
totalSold: 0,
|
| 35 |
+
currentStock: 0
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
const isArabic = language.code === 'ar';
|
| 39 |
+
|
| 40 |
+
useEffect(() => {
|
| 41 |
+
if (product) {
|
| 42 |
+
setFormData({
|
| 43 |
+
nameAr: product.nameAr,
|
| 44 |
+
nameEn: product.nameEn,
|
| 45 |
+
quantity: product.quantity,
|
| 46 |
+
price: product.price,
|
| 47 |
+
categoryAr: product.categoryAr,
|
| 48 |
+
categoryEn: product.categoryEn,
|
| 49 |
+
description: product.description || '',
|
| 50 |
+
secoCode: product.secoCode || '',
|
| 51 |
+
supplier: product.supplier || '',
|
| 52 |
+
supplierAr: product.supplierAr || '',
|
| 53 |
+
importDate: product.importDate || new Date().toISOString().split('T')[0],
|
| 54 |
+
inventoryDates: product.inventoryDates || {},
|
| 55 |
+
totalReceived: product.totalReceived || 0,
|
| 56 |
+
totalSold: product.totalSold || 0,
|
| 57 |
+
currentStock: product.currentStock || 0
|
| 58 |
+
});
|
| 59 |
+
} else {
|
| 60 |
+
setFormData({
|
| 61 |
+
nameAr: '',
|
| 62 |
+
nameEn: '',
|
| 63 |
+
quantity: 0,
|
| 64 |
+
price: 0,
|
| 65 |
+
categoryAr: '',
|
| 66 |
+
categoryEn: '',
|
| 67 |
+
description: '',
|
| 68 |
+
secoCode: '',
|
| 69 |
+
supplier: '',
|
| 70 |
+
supplierAr: '',
|
| 71 |
+
importDate: new Date().toISOString().split('T')[0],
|
| 72 |
+
inventoryDates: {},
|
| 73 |
+
totalReceived: 0,
|
| 74 |
+
totalSold: 0,
|
| 75 |
+
currentStock: 0
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
}, [product]);
|
| 79 |
+
|
| 80 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 81 |
+
e.preventDefault();
|
| 82 |
+
const newProduct: Product = {
|
| 83 |
+
id: product?.id || `product-${Date.now()}`,
|
| 84 |
+
...formData,
|
| 85 |
+
currentStock: formData.quantity,
|
| 86 |
+
totalReceived: formData.totalReceived || formData.quantity
|
| 87 |
+
};
|
| 88 |
+
onSave(newProduct);
|
| 89 |
+
onClose();
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
if (!isOpen) return null;
|
| 93 |
+
|
| 94 |
+
const labels = {
|
| 95 |
+
ar: {
|
| 96 |
+
title: product ? 'تعديل المنتج' : 'إضافة منتج جديد',
|
| 97 |
+
supplierAr: 'اسم المورد بالعربية',
|
| 98 |
+
supplier: 'اسم المورد بالإنجليزية',
|
| 99 |
+
nameAr: 'اسم المنتج بالعربية',
|
| 100 |
+
nameEn: 'اسم المنتج بالإنجليزية',
|
| 101 |
+
quantity: 'الكمية',
|
| 102 |
+
price: 'السعر',
|
| 103 |
+
categoryAr: 'الفئة بالعربية',
|
| 104 |
+
categoryEn: 'الفئة بالإنجليزية',
|
| 105 |
+
description: 'الوصف',
|
| 106 |
+
secoCode: 'كود SECO',
|
| 107 |
+
importDate: 'تاريخ الاستيراد',
|
| 108 |
+
totalReceived: 'إجمالي الوارد',
|
| 109 |
+
totalSold: 'إجمالي المباع',
|
| 110 |
+
save: 'حفظ',
|
| 111 |
+
cancel: 'إلغاء'
|
| 112 |
+
},
|
| 113 |
+
en: {
|
| 114 |
+
title: product ? 'Edit Product' : 'Add New Product',
|
| 115 |
+
supplierAr: 'Supplier Name (Arabic)',
|
| 116 |
+
supplier: 'Supplier Name (English)',
|
| 117 |
+
nameAr: 'Product Name (Arabic)',
|
| 118 |
+
nameEn: 'Product Name (English)',
|
| 119 |
+
quantity: 'Quantity',
|
| 120 |
+
price: 'Price',
|
| 121 |
+
categoryAr: 'Category (Arabic)',
|
| 122 |
+
categoryEn: 'Category (English)',
|
| 123 |
+
description: 'Description',
|
| 124 |
+
secoCode: 'SECO Code',
|
| 125 |
+
importDate: 'Import Date',
|
| 126 |
+
totalReceived: 'Total Received',
|
| 127 |
+
totalSold: 'Total Sold',
|
| 128 |
+
save: 'Save',
|
| 129 |
+
cancel: 'Cancel'
|
| 130 |
+
}
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
const t = labels[language.code];
|
| 134 |
+
|
| 135 |
+
return (
|
| 136 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
| 137 |
+
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
| 138 |
+
<div className="flex items-center justify-between p-6 border-b">
|
| 139 |
+
<h2 className="text-2xl font-bold text-gray-900">{t.title}</h2>
|
| 140 |
+
<button
|
| 141 |
+
onClick={onClose}
|
| 142 |
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
| 143 |
+
>
|
| 144 |
+
<X className="w-6 h-6" />
|
| 145 |
+
</button>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
| 149 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 150 |
+
<div>
|
| 151 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 152 |
+
{t.supplierAr}
|
| 153 |
+
</label>
|
| 154 |
+
<input
|
| 155 |
+
type="text"
|
| 156 |
+
value={formData.supplierAr}
|
| 157 |
+
onChange={(e) => setFormData({ ...formData, supplierAr: e.target.value })}
|
| 158 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 159 |
+
dir="rtl"
|
| 160 |
+
/>
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
<div>
|
| 164 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 165 |
+
{t.supplier}
|
| 166 |
+
</label>
|
| 167 |
+
<input
|
| 168 |
+
type="text"
|
| 169 |
+
value={formData.supplier}
|
| 170 |
+
onChange={(e) => setFormData({ ...formData, supplier: e.target.value })}
|
| 171 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 172 |
+
/>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<div>
|
| 176 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 177 |
+
{t.nameAr}
|
| 178 |
+
</label>
|
| 179 |
+
<input
|
| 180 |
+
type="text"
|
| 181 |
+
value={formData.nameAr}
|
| 182 |
+
onChange={(e) => setFormData({ ...formData, nameAr: e.target.value })}
|
| 183 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 184 |
+
required
|
| 185 |
+
dir="rtl"
|
| 186 |
+
/>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div>
|
| 190 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 191 |
+
{t.nameEn}
|
| 192 |
+
</label>
|
| 193 |
+
<input
|
| 194 |
+
type="text"
|
| 195 |
+
value={formData.nameEn}
|
| 196 |
+
onChange={(e) => setFormData({ ...formData, nameEn: e.target.value })}
|
| 197 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 198 |
+
required
|
| 199 |
+
/>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<div>
|
| 203 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 204 |
+
{t.quantity}
|
| 205 |
+
</label>
|
| 206 |
+
<input
|
| 207 |
+
type="number"
|
| 208 |
+
value={formData.quantity}
|
| 209 |
+
onChange={(e) => setFormData({ ...formData, quantity: Number(e.target.value) })}
|
| 210 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 211 |
+
required
|
| 212 |
+
min="0"
|
| 213 |
+
/>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<div>
|
| 217 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 218 |
+
{t.totalReceived}
|
| 219 |
+
</label>
|
| 220 |
+
<input
|
| 221 |
+
type="number"
|
| 222 |
+
value={formData.totalReceived}
|
| 223 |
+
onChange={(e) => setFormData({ ...formData, totalReceived: Number(e.target.value) })}
|
| 224 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 225 |
+
min="0"
|
| 226 |
+
/>
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
<div>
|
| 230 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 231 |
+
{t.totalSold}
|
| 232 |
+
</label>
|
| 233 |
+
<input
|
| 234 |
+
type="number"
|
| 235 |
+
value={formData.totalSold}
|
| 236 |
+
onChange={(e) => setFormData({ ...formData, totalSold: Number(e.target.value) })}
|
| 237 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 238 |
+
min="0"
|
| 239 |
+
/>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<div>
|
| 243 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 244 |
+
{t.importDate}
|
| 245 |
+
</label>
|
| 246 |
+
<input
|
| 247 |
+
type="date"
|
| 248 |
+
value={formData.importDate}
|
| 249 |
+
onChange={(e) => setFormData({ ...formData, importDate: e.target.value })}
|
| 250 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 251 |
+
/>
|
| 252 |
+
</div>
|
| 253 |
+
|
| 254 |
+
<div>
|
| 255 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 256 |
+
{t.price}
|
| 257 |
+
</label>
|
| 258 |
+
<input
|
| 259 |
+
type="number"
|
| 260 |
+
step="0.01"
|
| 261 |
+
value={formData.price}
|
| 262 |
+
onChange={(e) => setFormData({ ...formData, price: Number(e.target.value) })}
|
| 263 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 264 |
+
min="0"
|
| 265 |
+
/>
|
| 266 |
+
</div>
|
| 267 |
+
|
| 268 |
+
<div>
|
| 269 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 270 |
+
{t.categoryAr}
|
| 271 |
+
</label>
|
| 272 |
+
<input
|
| 273 |
+
type="text"
|
| 274 |
+
value={formData.categoryAr}
|
| 275 |
+
onChange={(e) => setFormData({ ...formData, categoryAr: e.target.value })}
|
| 276 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 277 |
+
required
|
| 278 |
+
dir="rtl"
|
| 279 |
+
/>
|
| 280 |
+
</div>
|
| 281 |
+
|
| 282 |
+
<div>
|
| 283 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 284 |
+
{t.categoryEn}
|
| 285 |
+
</label>
|
| 286 |
+
<input
|
| 287 |
+
type="text"
|
| 288 |
+
value={formData.categoryEn}
|
| 289 |
+
onChange={(e) => setFormData({ ...formData, categoryEn: e.target.value })}
|
| 290 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 291 |
+
required
|
| 292 |
+
/>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<div>
|
| 296 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 297 |
+
{t.secoCode}
|
| 298 |
+
</label>
|
| 299 |
+
<input
|
| 300 |
+
type="text"
|
| 301 |
+
value={formData.secoCode}
|
| 302 |
+
onChange={(e) => setFormData({ ...formData, secoCode: e.target.value })}
|
| 303 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 304 |
+
/>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div className="md:col-span-2">
|
| 308 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 309 |
+
{t.description}
|
| 310 |
+
</label>
|
| 311 |
+
<textarea
|
| 312 |
+
value={formData.description}
|
| 313 |
+
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
| 314 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
| 315 |
+
rows={3}
|
| 316 |
+
dir={isArabic ? 'rtl' : 'ltr'}
|
| 317 |
+
/>
|
| 318 |
+
</div>
|
| 319 |
+
</div>
|
| 320 |
+
|
| 321 |
+
<div className="flex justify-end gap-4 pt-6 border-t">
|
| 322 |
+
<button
|
| 323 |
+
type="button"
|
| 324 |
+
onClick={onClose}
|
| 325 |
+
className="px-6 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
| 326 |
+
>
|
| 327 |
+
{t.cancel}
|
| 328 |
+
</button>
|
| 329 |
+
<button
|
| 330 |
+
type="submit"
|
| 331 |
+
className="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 rounded-lg transition-colors flex items-center gap-2"
|
| 332 |
+
>
|
| 333 |
+
<Save className="w-4 h-4" />
|
| 334 |
+
{t.save}
|
| 335 |
+
</button>
|
| 336 |
+
</div>
|
| 337 |
+
</form>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
);
|
| 341 |
+
};
|
src/components/ProductModal.tsx
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { X, Package, Phone, Mail, CheckCircle, XCircle, Tag } from 'lucide-react';
|
| 3 |
+
import { CatalogProduct, Language } from '../types';
|
| 4 |
+
|
| 5 |
+
interface ProductModalProps {
|
| 6 |
+
product: CatalogProduct | null;
|
| 7 |
+
isOpen: boolean;
|
| 8 |
+
onClose: () => void;
|
| 9 |
+
language: Language;
|
| 10 |
+
onWhatsAppContact: (product: CatalogProduct) => void;
|
| 11 |
+
onEmailContact: (product: CatalogProduct) => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const ProductModal: React.FC<ProductModalProps> = ({
|
| 15 |
+
product,
|
| 16 |
+
isOpen,
|
| 17 |
+
onClose,
|
| 18 |
+
language,
|
| 19 |
+
onWhatsAppContact,
|
| 20 |
+
onEmailContact
|
| 21 |
+
}) => {
|
| 22 |
+
const isArabic = language.code === 'ar';
|
| 23 |
+
|
| 24 |
+
const labels = {
|
| 25 |
+
ar: {
|
| 26 |
+
productDetails: 'تفاصيل المنتج',
|
| 27 |
+
description: 'الوصف',
|
| 28 |
+
specifications: 'المواصفات',
|
| 29 |
+
category: 'الفئة',
|
| 30 |
+
availability: 'التوفر',
|
| 31 |
+
inStock: 'متوفر',
|
| 32 |
+
outOfStock: 'غير متوفر',
|
| 33 |
+
secoCode: 'كود SECO',
|
| 34 |
+
contactForPurchase: 'اتصل للشراء',
|
| 35 |
+
whatsapp: 'واتساب',
|
| 36 |
+
email: 'إيميل',
|
| 37 |
+
close: 'إغلاق'
|
| 38 |
+
},
|
| 39 |
+
en: {
|
| 40 |
+
productDetails: 'Product Details',
|
| 41 |
+
description: 'Description',
|
| 42 |
+
specifications: 'Specifications',
|
| 43 |
+
category: 'Category',
|
| 44 |
+
availability: 'Availability',
|
| 45 |
+
inStock: 'In Stock',
|
| 46 |
+
outOfStock: 'Out of Stock',
|
| 47 |
+
secoCode: 'SECO Code',
|
| 48 |
+
contactForPurchase: 'Contact for Purchase',
|
| 49 |
+
whatsapp: 'WhatsApp',
|
| 50 |
+
email: 'Email',
|
| 51 |
+
close: 'Close'
|
| 52 |
+
}
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const t = labels[language.code];
|
| 56 |
+
|
| 57 |
+
if (!isOpen || !product) return null;
|
| 58 |
+
|
| 59 |
+
return (
|
| 60 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
| 61 |
+
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
| 62 |
+
{/* Header */}
|
| 63 |
+
<div className="flex items-center justify-between p-6 border-b">
|
| 64 |
+
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
|
| 65 |
+
<Package className="w-6 h-6 text-blue-600" />
|
| 66 |
+
{t.productDetails}
|
| 67 |
+
</h2>
|
| 68 |
+
<button
|
| 69 |
+
onClick={onClose}
|
| 70 |
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
| 71 |
+
>
|
| 72 |
+
<X className="w-6 h-6" />
|
| 73 |
+
</button>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
{/* Content */}
|
| 77 |
+
<div className="p-6 space-y-6">
|
| 78 |
+
{/* Product Image */}
|
| 79 |
+
<div className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center">
|
| 80 |
+
<Package className="w-24 h-24 text-gray-400" />
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
{/* Product Info */}
|
| 84 |
+
<div>
|
| 85 |
+
<div className="flex items-start justify-between mb-4">
|
| 86 |
+
<div className="flex-1">
|
| 87 |
+
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
| 88 |
+
{product.name}
|
| 89 |
+
</h3>
|
| 90 |
+
<div className="flex items-center gap-4 mb-2">
|
| 91 |
+
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
| 92 |
+
<Tag className="w-4 h-4 mr-1" />
|
| 93 |
+
{product.category}
|
| 94 |
+
</span>
|
| 95 |
+
{product.inStock ? (
|
| 96 |
+
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
| 97 |
+
<CheckCircle className="w-4 h-4 mr-1" />
|
| 98 |
+
{t.inStock}
|
| 99 |
+
</span>
|
| 100 |
+
) : (
|
| 101 |
+
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
|
| 102 |
+
<XCircle className="w-4 h-4 mr-1" />
|
| 103 |
+
{t.outOfStock}
|
| 104 |
+
</span>
|
| 105 |
+
)}
|
| 106 |
+
</div>
|
| 107 |
+
{product.secoCode && (
|
| 108 |
+
<p className="text-sm text-gray-600">
|
| 109 |
+
<span className="font-medium">{t.secoCode}:</span> {product.secoCode}
|
| 110 |
+
</p>
|
| 111 |
+
)}
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
{/* Description */}
|
| 116 |
+
<div className="mb-6">
|
| 117 |
+
<h4 className="text-lg font-semibold text-gray-900 mb-2">{t.description}</h4>
|
| 118 |
+
<p className="text-gray-700 leading-relaxed">{product.description}</p>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
{/* Specifications */}
|
| 122 |
+
{product.specifications && product.specifications.length > 0 && (
|
| 123 |
+
<div className="mb-6">
|
| 124 |
+
<h4 className="text-lg font-semibold text-gray-900 mb-3">{t.specifications}</h4>
|
| 125 |
+
<div className="bg-gray-50 rounded-lg p-4">
|
| 126 |
+
<ul className="space-y-2">
|
| 127 |
+
{product.specifications.map((spec, index) => (
|
| 128 |
+
<li key={index} className="flex items-center gap-2 text-gray-700">
|
| 129 |
+
<div className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></div>
|
| 130 |
+
{spec}
|
| 131 |
+
</li>
|
| 132 |
+
))}
|
| 133 |
+
</ul>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
)}
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
{/* Footer */}
|
| 141 |
+
<div className="border-t p-6">
|
| 142 |
+
<div className="mb-4">
|
| 143 |
+
<h4 className="text-lg font-semibold text-gray-900 mb-3">{t.contactForPurchase}</h4>
|
| 144 |
+
<div className="bg-blue-50 rounded-lg p-4">
|
| 145 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm text-gray-700">
|
| 146 |
+
<div className="flex items-center gap-2">
|
| 147 |
+
<Phone className="w-4 h-4 text-blue-600" />
|
| 148 |
+
<span>+1872752725</span>
|
| 149 |
+
</div>
|
| 150 |
+
<div className="flex items-center gap-2">
|
| 151 |
+
<Mail className="w-4 h-4 text-blue-600" />
|
| 152 |
+
<span>saif@gmail.com</span>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div className="flex gap-3">
|
| 159 |
+
<button
|
| 160 |
+
onClick={() => onWhatsAppContact(product)}
|
| 161 |
+
className="flex-1 bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
| 162 |
+
>
|
| 163 |
+
<Phone className="w-5 h-5" />
|
| 164 |
+
{t.whatsapp}
|
| 165 |
+
</button>
|
| 166 |
+
<button
|
| 167 |
+
onClick={() => onEmailContact(product)}
|
| 168 |
+
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
|
| 169 |
+
>
|
| 170 |
+
<Mail className="w-5 h-5" />
|
| 171 |
+
{t.email}
|
| 172 |
+
</button>
|
| 173 |
+
<button
|
| 174 |
+
onClick={onClose}
|
| 175 |
+
className="px-6 py-3 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors font-medium"
|
| 176 |
+
>
|
| 177 |
+
{t.close}
|
| 178 |
+
</button>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
);
|
| 184 |
+
};
|
src/data/catalogProducts.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { CatalogProduct } from '../types';
|
| 2 |
+
|
| 3 |
+
export const catalogProducts: CatalogProduct[] = [
|
| 4 |
+
{
|
| 5 |
+
id: 'lock-001',
|
| 6 |
+
name: 'LOCK,PAD,OPERATION,6MM,40MM,17.2MM,BRS',
|
| 7 |
+
description: 'Brass padlock for operational use with 6mm shackle diameter',
|
| 8 |
+
category: 'Locks & Security',
|
| 9 |
+
inStock: true,
|
| 10 |
+
specifications: ['6mm shackle diameter', '40mm width', '17.2mm height', 'Brass construction']
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
id: 'lock-002',
|
| 14 |
+
name: 'LOCK,PAD,SAFETY,6MM,40MM,17.2MM,BRS/RED',
|
| 15 |
+
description: 'Safety padlock in brass/red finish for lockout/tagout procedures',
|
| 16 |
+
category: 'Locks & Security',
|
| 17 |
+
inStock: true,
|
| 18 |
+
specifications: ['6mm shackle diameter', '40mm width', '17.2mm height', 'Brass/Red finish', 'Safety rated']
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
id: 'lock-003',
|
| 22 |
+
name: 'LOCK,CYLINDER,BRS,17WDX45DPX33MM HT',
|
| 23 |
+
description: 'Brass cylinder lock with specific dimensions',
|
| 24 |
+
category: 'Locks & Security',
|
| 25 |
+
inStock: true,
|
| 26 |
+
specifications: ['17mm width', '45mm depth', '33mm height', 'Brass construction']
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
id: 'key-001',
|
| 30 |
+
name: 'KEY,MASTER,BRS,47MMX2MM THK',
|
| 31 |
+
description: 'Master key in brass construction',
|
| 32 |
+
category: 'Keys & Accessories',
|
| 33 |
+
inStock: true,
|
| 34 |
+
specifications: ['47mm length', '2mm thickness', 'Brass construction', 'Master key type']
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
id: 'oil-001',
|
| 38 |
+
name: 'OIL,INSUL,TRANSFORMER,0.19 SPGR,30KV',
|
| 39 |
+
description: 'Transformer insulating oil with specific gravity 0.19',
|
| 40 |
+
category: 'Electrical Fluids',
|
| 41 |
+
inStock: true,
|
| 42 |
+
specifications: ['0.19 specific gravity', '30KV rating', 'Transformer grade', 'Insulating properties']
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
id: 'cable-001',
|
| 46 |
+
name: 'CABLE,PWR,600V/1KV,CU,1C,35MM2,XLPE',
|
| 47 |
+
description: 'Single core copper power cable with XLPE insulation',
|
| 48 |
+
category: 'Power Cables',
|
| 49 |
+
inStock: true,
|
| 50 |
+
specifications: ['600V/1KV rating', 'Copper conductor', '35mm² cross-section', 'XLPE insulation']
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
id: 'cable-002',
|
| 54 |
+
name: 'CABLE,PWR,600V/1KV,CU,1C,120MM2,XLPE',
|
| 55 |
+
description: 'Single core copper power cable 120mm² with XLPE insulation',
|
| 56 |
+
category: 'Power Cables',
|
| 57 |
+
inStock: true,
|
| 58 |
+
specifications: ['600V/1KV rating', 'Copper conductor', '120mm² cross-section', 'XLPE insulation']
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
id: 'cable-003',
|
| 62 |
+
name: 'CABLE,PWR,600V/1KV,AL,4C,70MM2,XLPE',
|
| 63 |
+
description: '4-core aluminum power cable with XLPE insulation',
|
| 64 |
+
category: 'Power Cables',
|
| 65 |
+
inStock: true,
|
| 66 |
+
specifications: ['600V/1KV rating', 'Aluminum conductor', '4-core', '70mm² cross-section', 'XLPE insulation']
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
id: 'cable-004',
|
| 70 |
+
name: 'CABLE,PWR,15KV,CU,3C,300/35MM2,XPLE,ARM',
|
| 71 |
+
description: '15KV armored copper power cable, 3-core',
|
| 72 |
+
category: 'High Voltage Cables',
|
| 73 |
+
inStock: true,
|
| 74 |
+
specifications: ['15KV rating', 'Copper conductor', '3-core', '300/35mm²', 'Armored construction']
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
id: 'joint-001',
|
| 78 |
+
name: 'JOINT KIT,STR,1KV,4X70MM2,AL,UAR',
|
| 79 |
+
description: 'Straight joint kit for 1KV aluminum cables',
|
| 80 |
+
category: 'Cable Accessories',
|
| 81 |
+
inStock: true,
|
| 82 |
+
specifications: ['1KV rating', '4x70mm²', 'Aluminum conductor', 'Straight joint', 'UAR type']
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
id: 'term-001',
|
| 86 |
+
name: 'TERM KIT,STR,15KV,3X185/35MM2,CU',
|
| 87 |
+
description: 'Straight termination kit for 15KV copper cables',
|
| 88 |
+
category: 'Cable Accessories',
|
| 89 |
+
inStock: true,
|
| 90 |
+
specifications: ['15KV rating', '3x185/35mm²', 'Copper conductor', 'Straight termination']
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
id: 'connector-001',
|
| 94 |
+
name: 'CONNECTOR,LUG,35MM2 CU,(1)M10 BLT HL',
|
| 95 |
+
description: 'Copper cable lug connector with bolt hole',
|
| 96 |
+
category: 'Electrical Connectors',
|
| 97 |
+
inStock: true,
|
| 98 |
+
specifications: ['35mm² capacity', 'Copper construction', 'M10 bolt hole', 'Single bolt']
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
id: 'insulator-001',
|
| 102 |
+
name: 'INSULATOR,POST,PORC,13.8KV,152MM D,552MM',
|
| 103 |
+
description: 'Porcelain post insulator for 13.8KV applications',
|
| 104 |
+
category: 'Insulators',
|
| 105 |
+
inStock: true,
|
| 106 |
+
specifications: ['13.8KV rating', '152mm diameter', '552mm height', 'Porcelain construction']
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
id: 'pole-001',
|
| 110 |
+
name: 'POLE,PWR,DIST,OC10,STL,10M LG',
|
| 111 |
+
description: 'Steel distribution pole, 10 meters length',
|
| 112 |
+
category: 'Poles & Structures',
|
| 113 |
+
inStock: true,
|
| 114 |
+
specifications: ['Steel construction', '10m length', 'Distribution class', 'OC10 type']
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
id: 'switch-001',
|
| 118 |
+
name: 'SWITCH,OHD,VERTICAL,13.8KV,3P,400A',
|
| 119 |
+
description: 'Overhead vertical switch for 13.8KV systems',
|
| 120 |
+
category: 'Switching Equipment',
|
| 121 |
+
inStock: true,
|
| 122 |
+
specifications: ['13.8KV rating', '3-pole', '400A capacity', 'Vertical mounting', 'Overhead type']
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
id: 'panel-001',
|
| 126 |
+
name: 'PANEL,DIST,1600A,4-400A CB',
|
| 127 |
+
description: 'Distribution panel with 1600A main and 4x400A circuit breakers',
|
| 128 |
+
category: 'Distribution Panels',
|
| 129 |
+
inStock: true,
|
| 130 |
+
specifications: ['1600A main capacity', '4x400A circuit breakers', 'Distribution type']
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
id: 'transformer-001',
|
| 134 |
+
name: 'TFMR,PD,500KVA,13.8KX400/231V,95KVB',
|
| 135 |
+
description: 'Pad-mounted distribution transformer 500KVA',
|
| 136 |
+
category: 'Transformers',
|
| 137 |
+
inStock: true,
|
| 138 |
+
specifications: ['500KVA capacity', '13.8KV primary', '400/231V secondary', '95KV BIL', 'Pad-mounted']
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
id: 'capacitor-001',
|
| 142 |
+
name: 'CAPACITOR BANK,450KVAR,15KV,95KV BIL,FX',
|
| 143 |
+
description: 'Fixed capacitor bank for power factor correction',
|
| 144 |
+
category: 'Capacitors',
|
| 145 |
+
inStock: true,
|
| 146 |
+
specifications: ['450KVAR capacity', '15KV rating', '95KV BIL', 'Fixed type']
|
| 147 |
+
},
|
| 148 |
+
{
|
| 149 |
+
id: 'arrester-001',
|
| 150 |
+
name: 'ARRESTER,SURGE,15KV,345MM CRP,5KA OPR',
|
| 151 |
+
description: 'Surge arrester for 15KV systems',
|
| 152 |
+
category: 'Protection Equipment',
|
| 153 |
+
inStock: true,
|
| 154 |
+
specifications: ['15KV rating', '345mm creepage', '5KA operation', 'Surge protection']
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
id: 'meter-001',
|
| 158 |
+
name: 'METER,KWH,3×20(100)A,3P,220/127&380/220V',
|
| 159 |
+
description: 'Three-phase energy meter for revenue metering',
|
| 160 |
+
category: 'Metering Equipment',
|
| 161 |
+
inStock: true,
|
| 162 |
+
specifications: ['3-phase', '20(100)A rating', '220/127 & 380/220V', 'kWh measurement']
|
| 163 |
+
}
|
| 164 |
+
];
|
| 165 |
+
|
| 166 |
+
export const productCategories = [
|
| 167 |
+
'All Categories',
|
| 168 |
+
'Locks & Security',
|
| 169 |
+
'Keys & Accessories',
|
| 170 |
+
'Electrical Fluids',
|
| 171 |
+
'Power Cables',
|
| 172 |
+
'High Voltage Cables',
|
| 173 |
+
'Cable Accessories',
|
| 174 |
+
'Electrical Connectors',
|
| 175 |
+
'Insulators',
|
| 176 |
+
'Poles & Structures',
|
| 177 |
+
'Switching Equipment',
|
| 178 |
+
'Distribution Panels',
|
| 179 |
+
'Transformers',
|
| 180 |
+
'Capacitors',
|
| 181 |
+
'Protection Equipment',
|
| 182 |
+
'Metering Equipment'
|
| 183 |
+
];
|
src/index.css
CHANGED
|
@@ -1,13 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
| 5 |
-
sans-serif;
|
| 6 |
-
-webkit-font-smoothing: antialiased;
|
| 7 |
-
-moz-osx-font-smoothing: grayscale;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
code {
|
| 11 |
-
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
| 12 |
-
monospace;
|
| 13 |
-
}
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react';
|
| 2 |
+
import { createRoot } from 'react-dom/client';
|
| 3 |
+
import App from './App.tsx';
|
| 4 |
+
import './index.css';
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>
|
| 10 |
+
);
|
src/types/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Product {
|
| 2 |
+
id: string;
|
| 3 |
+
nameAr: string;
|
| 4 |
+
nameEn: string;
|
| 5 |
+
quantity: number;
|
| 6 |
+
price: number;
|
| 7 |
+
categoryAr: string;
|
| 8 |
+
categoryEn: string;
|
| 9 |
+
description?: string;
|
| 10 |
+
secoCode?: string;
|
| 11 |
+
supplier?: string;
|
| 12 |
+
supplierAr?: string;
|
| 13 |
+
importDate?: string;
|
| 14 |
+
inventoryDates?: { [date: string]: number };
|
| 15 |
+
totalReceived?: number;
|
| 16 |
+
totalSold?: number;
|
| 17 |
+
currentStock?: number;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export interface Language {
|
| 21 |
+
code: 'ar' | 'en';
|
| 22 |
+
name: string;
|
| 23 |
+
dir: 'rtl' | 'ltr';
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export const languages: Language[] = [
|
| 27 |
+
{ code: 'ar', name: 'العربية', dir: 'rtl' },
|
| 28 |
+
{ code: 'en', name: 'English', dir: 'ltr' }
|
| 29 |
+
];
|
| 30 |
+
|
| 31 |
+
export interface CatalogProduct {
|
| 32 |
+
id: string;
|
| 33 |
+
name: string;
|
| 34 |
+
description: string;
|
| 35 |
+
category: string;
|
| 36 |
+
price?: number;
|
| 37 |
+
inStock: boolean;
|
| 38 |
+
secoCode?: string;
|
| 39 |
+
specifications?: string[];
|
| 40 |
+
image?: string;
|
| 41 |
+
}
|
src/utils/excelUtils.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as XLSX from 'xlsx';
|
| 2 |
+
import { Product } from '../types';
|
| 3 |
+
|
| 4 |
+
export const readExcelFile = (file: File): Promise<Product[]> => {
|
| 5 |
+
return new Promise((resolve, reject) => {
|
| 6 |
+
const reader = new FileReader();
|
| 7 |
+
|
| 8 |
+
reader.onload = (e) => {
|
| 9 |
+
try {
|
| 10 |
+
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
| 11 |
+
const workbook = XLSX.read(data, { type: 'array' });
|
| 12 |
+
const sheetName = workbook.SheetNames[0];
|
| 13 |
+
const worksheet = workbook.Sheets[sheetName];
|
| 14 |
+
|
| 15 |
+
// Convert to JSON with header row
|
| 16 |
+
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
| 17 |
+
|
| 18 |
+
if (jsonData.length < 2) {
|
| 19 |
+
reject(new Error('Excel file must contain at least one data row'));
|
| 20 |
+
return;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Parse the data into Product objects
|
| 24 |
+
const products: Product[] = [];
|
| 25 |
+
const headers = jsonData[0] as string[];
|
| 26 |
+
|
| 27 |
+
for (let i = 1; i < jsonData.length; i++) {
|
| 28 |
+
const row = jsonData[i] as any[];
|
| 29 |
+
if (row.some(cell => cell !== undefined && cell !== '')) {
|
| 30 |
+
const product: Product = {
|
| 31 |
+
id: `product-${Date.now()}-${i}`,
|
| 32 |
+
nameAr: row[0] || 'منتج غير محدد',
|
| 33 |
+
nameEn: row[1] || 'Unnamed Product',
|
| 34 |
+
quantity: Number(row[2]) || 0,
|
| 35 |
+
price: Number(row[3]) || 0,
|
| 36 |
+
categoryAr: row[4] || 'عام',
|
| 37 |
+
categoryEn: row[5] || 'General',
|
| 38 |
+
description: row[6] || '',
|
| 39 |
+
secoCode: row[7] || ''
|
| 40 |
+
};
|
| 41 |
+
products.push(product);
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
resolve(products);
|
| 46 |
+
} catch (error) {
|
| 47 |
+
reject(error);
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
reader.onerror = () => reject(new Error('Failed to read file'));
|
| 52 |
+
reader.readAsArrayBuffer(file);
|
| 53 |
+
});
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
export const downloadExcelFile = (products: Product[], filename: string = 'inventory') => {
|
| 57 |
+
// Get all unique dates from all products
|
| 58 |
+
const allDates = new Set<string>();
|
| 59 |
+
products.forEach(product => {
|
| 60 |
+
if (product.inventoryDates) {
|
| 61 |
+
Object.keys(product.inventoryDates).forEach(date => allDates.add(date));
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
const sortedDates = Array.from(allDates).sort();
|
| 66 |
+
|
| 67 |
+
// Create worksheet data
|
| 68 |
+
const headers = [
|
| 69 |
+
'المورد', 'Supplier', 'اسم المنتج', 'Product Name',
|
| 70 |
+
...sortedDates,
|
| 71 |
+
'المجموع', 'Total', 'المباع', 'Sold', 'المتبقي', 'Remaining',
|
| 72 |
+
'الوصف', 'Description', 'كود SECO', 'SECO Code'
|
| 73 |
+
];
|
| 74 |
+
|
| 75 |
+
const wsData = [
|
| 76 |
+
headers,
|
| 77 |
+
...products.map(product => {
|
| 78 |
+
const row = [
|
| 79 |
+
product.supplierAr || 'مورد غير محدد',
|
| 80 |
+
product.supplier || 'Unknown Supplier',
|
| 81 |
+
product.nameAr,
|
| 82 |
+
product.nameEn || product.nameAr
|
| 83 |
+
];
|
| 84 |
+
|
| 85 |
+
// Add inventory quantities for each date
|
| 86 |
+
sortedDates.forEach(date => {
|
| 87 |
+
row.push(product.inventoryDates?.[date] || 0);
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
// Add totals
|
| 91 |
+
row.push(
|
| 92 |
+
product.totalReceived || 0,
|
| 93 |
+
product.totalReceived || 0,
|
| 94 |
+
product.totalSold || 0,
|
| 95 |
+
product.totalSold || 0,
|
| 96 |
+
product.currentStock || product.quantity,
|
| 97 |
+
product.currentStock || product.quantity,
|
| 98 |
+
product.description || '',
|
| 99 |
+
product.description || '',
|
| 100 |
+
product.secoCode || '',
|
| 101 |
+
product.secoCode || ''
|
| 102 |
+
);
|
| 103 |
+
|
| 104 |
+
return row;
|
| 105 |
+
})
|
| 106 |
+
];
|
| 107 |
+
|
| 108 |
+
// Create workbook and worksheet
|
| 109 |
+
const wb = XLSX.utils.book_new();
|
| 110 |
+
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
| 111 |
+
|
| 112 |
+
// Set column widths
|
| 113 |
+
const colWidths = headers.map((header, index) => {
|
| 114 |
+
if (header.includes('وصف') || header.includes('Description')) return { wch: 25 };
|
| 115 |
+
if (header.includes('منتج') || header.includes('Product') || header.includes('مورد') || header.includes('Supplier')) return { wch: 20 };
|
| 116 |
+
if (header.includes('/') || header.match(/\d/)) return { wch: 12 }; // Date columns
|
| 117 |
+
return { wch: 15 };
|
| 118 |
+
});
|
| 119 |
+
ws['!cols'] = colWidths;
|
| 120 |
+
|
| 121 |
+
// Add worksheet to workbook
|
| 122 |
+
XLSX.utils.book_append_sheet(wb, ws, 'المخزون - Inventory');
|
| 123 |
+
|
| 124 |
+
// Generate and download file
|
| 125 |
+
const timestamp = new Date().toISOString().split('T')[0];
|
| 126 |
+
XLSX.writeFile(wb, `${filename}_${timestamp}.xlsx`);
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
export const createSampleData = (): Product[] => {
|
| 130 |
+
return [
|
| 131 |
+
{
|
| 132 |
+
id: 'sample-1',
|
| 133 |
+
supplierAr: 'شركة التقنية المتقدمة',
|
| 134 |
+
supplier: 'Advanced Tech Company',
|
| 135 |
+
nameAr: 'لابتوب ديل',
|
| 136 |
+
nameEn: 'Dell Laptop',
|
| 137 |
+
quantity: 15,
|
| 138 |
+
currentStock: 15,
|
| 139 |
+
totalReceived: 20,
|
| 140 |
+
totalSold: 5,
|
| 141 |
+
price: 2500,
|
| 142 |
+
categoryAr: 'إلكترونيات',
|
| 143 |
+
categoryEn: 'Electronics',
|
| 144 |
+
description: 'لابتوب عالي الأداء',
|
| 145 |
+
secoCode: 'ELEC001',
|
| 146 |
+
importDate: '2025-01-01',
|
| 147 |
+
inventoryDates: {
|
| 148 |
+
'27/8/2025': 10,
|
| 149 |
+
'8/4/2025': 5,
|
| 150 |
+
'7/31/2025': 5
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
id: 'sample-2',
|
| 155 |
+
supplierAr: 'مؤسسة الملحقات الذكية',
|
| 156 |
+
supplier: 'Smart Accessories Corp',
|
| 157 |
+
nameAr: 'ماوس لاسلكي',
|
| 158 |
+
nameEn: 'Wireless Mouse',
|
| 159 |
+
quantity: 50,
|
| 160 |
+
currentStock: 50,
|
| 161 |
+
totalReceived: 60,
|
| 162 |
+
totalSold: 10,
|
| 163 |
+
price: 75,
|
| 164 |
+
categoryAr: 'ملحقات',
|
| 165 |
+
categoryEn: 'Accessories',
|
| 166 |
+
description: 'ماوس لاسلكي مريح',
|
| 167 |
+
secoCode: 'ACC002',
|
| 168 |
+
importDate: '2025-01-15',
|
| 169 |
+
inventoryDates: {
|
| 170 |
+
'7/26/2025': 30,
|
| 171 |
+
'27/8/2025': 20,
|
| 172 |
+
'8/6/2025': 10
|
| 173 |
+
}
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
id: 'sample-3',
|
| 177 |
+
supplierAr: 'متجر الألعاب الإلكترونية',
|
| 178 |
+
supplier: 'Gaming Electronics Store',
|
| 179 |
+
nameAr: 'كيبورد ميكانيكي',
|
| 180 |
+
nameEn: 'Mechanical Keyboard',
|
| 181 |
+
quantity: 25,
|
| 182 |
+
currentStock: 25,
|
| 183 |
+
totalReceived: 30,
|
| 184 |
+
totalSold: 5,
|
| 185 |
+
price: 150,
|
| 186 |
+
categoryAr: 'ملحقات',
|
| 187 |
+
categoryEn: 'Accessories',
|
| 188 |
+
description: 'كيبورد ميكانيكي للألعاب',
|
| 189 |
+
secoCode: 'ACC003',
|
| 190 |
+
importDate: '2025-02-01',
|
| 191 |
+
inventoryDates: {
|
| 192 |
+
'1/9/2025': 15,
|
| 193 |
+
'7/15/2025': 10,
|
| 194 |
+
'8/3/2025': 5
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
];
|
| 198 |
+
};
|
src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
| 4 |
+
theme: {
|
| 5 |
+
extend: {},
|
| 6 |
+
},
|
| 7 |
+
plugins: [],
|
| 8 |
+
};
|
tsconfig.app.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
|
| 9 |
+
/* Bundler mode */
|
| 10 |
+
"moduleResolution": "bundler",
|
| 11 |
+
"allowImportingTsExtensions": true,
|
| 12 |
+
"isolatedModules": true,
|
| 13 |
+
"moduleDetection": "force",
|
| 14 |
+
"noEmit": true,
|
| 15 |
+
"jsx": "react-jsx",
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"noFallthroughCasesInSwitch": true
|
| 22 |
+
},
|
| 23 |
+
"include": ["src"]
|
| 24 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
tsconfig.node.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"lib": ["ES2023"],
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
|
| 8 |
+
/* Bundler mode */
|
| 9 |
+
"moduleResolution": "bundler",
|
| 10 |
+
"allowImportingTsExtensions": true,
|
| 11 |
+
"isolatedModules": true,
|
| 12 |
+
"moduleDetection": "force",
|
| 13 |
+
"noEmit": true,
|
| 14 |
+
|
| 15 |
+
/* Linting */
|
| 16 |
+
"strict": true,
|
| 17 |
+
"noUnusedLocals": true,
|
| 18 |
+
"noUnusedParameters": true,
|
| 19 |
+
"noFallthroughCasesInSwitch": true
|
| 20 |
+
},
|
| 21 |
+
"include": ["vite.config.ts"]
|
| 22 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
|
| 4 |
+
// https://vitejs.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
optimizeDeps: {
|
| 8 |
+
exclude: ['lucide-react'],
|
| 9 |
+
},
|
| 10 |
+
});
|