precison9 commited on
Commit
6b154f5
·
1 Parent(s): d2118c0

Add/replace Space with my React Vite TypeScript app

Browse files
.gitignore CHANGED
@@ -1,23 +1,23 @@
1
- # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
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-template",
3
- "version": "0.1.0",
4
  "private": true,
5
- "dependencies": {
6
- "@testing-library/dom": "^10.4.0",
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
- "start": "react-scripts start",
17
- "build": "react-scripts build",
18
- "test": "react-scripts test",
19
- "eject": "react-scripts eject"
 
20
  },
21
- "eslintConfig": {
22
- "extends": [
23
- "react-app",
24
- "react-app/jest"
25
- ]
 
26
  },
27
- "browserslist": {
28
- "production": [
29
- ">0.2%",
30
- "not dead",
31
- "not op_mini all"
32
- ],
33
- "development": [
34
- "last 1 chrome version",
35
- "last 1 firefox version",
36
- "last 1 safari version"
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
- body {
2
- margin: 0;
3
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
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
+ });