init project
This commit is contained in:
184
.gitignore
vendored
Normal file
184
.gitignore
vendored
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Local development files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Package manager lock files (uncomment if you want to ignore them)
|
||||||
|
# package-lock.json
|
||||||
|
# yarn.lock
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Database files (if you add database support later)
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# PDF output directory (if you want to ignore generated PDFs)
|
||||||
|
/pdfs/
|
||||||
|
/invoices/
|
||||||
|
|
||||||
|
# Custom application specific
|
||||||
|
/config/local.js
|
||||||
|
/public/uploads/
|
||||||
98
README.md
Normal file
98
README.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# نرمافزار حسابداری هلو-مانند
|
||||||
|
|
||||||
|
یک نرمافزار حسابداری کامل با ویژگیهای مشابه هلو، با رابط کاربری فارسی و طراحی مدرن.
|
||||||
|
|
||||||
|
## ویژگیهای اصلی
|
||||||
|
|
||||||
|
### مدیریت پایه
|
||||||
|
- **تعریف شخص**: مدیریت مشتریان و تامینکنندگان
|
||||||
|
- **تعریف کالا**: مدیریت کالاها و محصولات با کد و قیمت
|
||||||
|
- **خرید**: ثبت و مدیریت خریدها با وضعیتهای مختلف
|
||||||
|
- **فروش**: ثبت و مدیریت فروشها با محاسبه درآمد
|
||||||
|
- **انبارداری**: مدیریت موجودی، تنظیم موجودی و هشدار کمبود
|
||||||
|
|
||||||
|
### ویژگیهای پیشرفته (مشابه هلو)
|
||||||
|
- **نمودار حسابها**: سیستم کامل حسابهای کل و تفصیلی
|
||||||
|
- **گزارشهای مالی**: ترازنامه، صورت سود و زیان، تراز آزمایشی
|
||||||
|
- **سیستم فاکتور**: ایجاد و مدیریت فاکتورها با محاسبه مالیات
|
||||||
|
- **پشتیبانی از چندین نوع حساب**: دارایی، بدهی، حقوق صاحبان سهام، درآمد، هزینه
|
||||||
|
|
||||||
|
## تکنولوژیهای استفاده شده
|
||||||
|
|
||||||
|
- React 18
|
||||||
|
- React Router DOM
|
||||||
|
- Tailwind CSS
|
||||||
|
- فونت فارسی Vazir
|
||||||
|
|
||||||
|
## نصب و راهاندازی
|
||||||
|
|
||||||
|
1. نصب وابستگیها:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. اجرای پروژه:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. باز کردن مرورگر در آدرس:
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## ساختار پروژه
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ ├── Dashboard.js # داشبورد اصلی
|
||||||
|
│ ├── PersonManagement.js # مدیریت اشخاص
|
||||||
|
│ ├── ProductManagement.js # مدیریت کالاها
|
||||||
|
│ ├── PurchaseManagement.js # مدیریت خرید
|
||||||
|
│ ├── SalesManagement.js # مدیریت فروش
|
||||||
|
│ ├── InventoryManagement.js # مدیریت انبار
|
||||||
|
│ ├── ChartOfAccounts.js # نمودار حسابها
|
||||||
|
│ ├── FinancialReports.js # گزارشهای مالی
|
||||||
|
│ └── InvoiceSystem.js # سیستم فاکتور
|
||||||
|
├── App.js # کامپوننت اصلی
|
||||||
|
├── App.css # استایلهای سفارشی
|
||||||
|
├── index.js # نقطه ورود
|
||||||
|
└── index.css # استایلهای اصلی
|
||||||
|
```
|
||||||
|
|
||||||
|
## ویژگیهای رابط کاربری
|
||||||
|
|
||||||
|
- طراحی ریسپانسیو (Responsive)
|
||||||
|
- پشتیبانی کامل از زبان فارسی
|
||||||
|
- جهت متن راست به چپ (RTL)
|
||||||
|
- استفاده از فونت فارسی Vazir
|
||||||
|
- طراحی مدرن با Tailwind CSS
|
||||||
|
|
||||||
|
## نحوه استفاده
|
||||||
|
|
||||||
|
### بخشهای اصلی
|
||||||
|
1. **داشبورد**: نمای کلی از آمار و عملیات سریع
|
||||||
|
2. **تعریف شخص**: افزودن، ویرایش و حذف مشتریان و تامینکنندگان
|
||||||
|
3. **تعریف کالا**: مدیریت کالاها با قیمت، موجودی و کد
|
||||||
|
4. **خرید**: ثبت خریدهای جدید و مدیریت وضعیت آنها
|
||||||
|
5. **فروش**: ثبت فروشها و محاسبه درآمد
|
||||||
|
6. **انبارداری**: نظارت بر موجودی و تنظیم آن
|
||||||
|
|
||||||
|
### ویژگیهای پیشرفته (مشابه هلو)
|
||||||
|
7. **نمودار حسابها**: ایجاد و مدیریت ساختار حسابهای کل و تفصیلی
|
||||||
|
8. **گزارشهای مالی**:
|
||||||
|
- ترازنامه با بررسی تعادل
|
||||||
|
- صورت سود و زیان با محاسبه سود/زیان خالص
|
||||||
|
- تراز آزمایشی با ماندههای بدهکار و بستانکار
|
||||||
|
9. **سیستم فاکتور**:
|
||||||
|
- ایجاد فاکتورهای چندقلمی
|
||||||
|
- محاسبه خودکار مالیات
|
||||||
|
- مدیریت وضعیت پرداخت
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- تمام دادهها در حافظه مرورگر ذخیره میشوند
|
||||||
|
- برای ذخیره دائمی دادهها، نیاز به اتصال به پایگاه داده است
|
||||||
|
- قیمتها به ریال نمایش داده میشوند
|
||||||
|
- تاریخها به تقویم شمسی نمایش داده میشوند
|
||||||
20646
package-lock.json
generated
Normal file
20646
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "farsi-accounting-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
|
"@testing-library/react": "^13.3.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"jspdf": "^2.5.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.3.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.7",
|
||||||
|
"postcss": "^8.4.14",
|
||||||
|
"tailwindcss": "^3.1.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
21
public/index.html
Normal file
21
public/index.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="نرمافزار حسابداری ساده"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazir-font@v30.1.0/dist/font-face.css" rel="stylesheet" type="text/css" />
|
||||||
|
<title>نرمافزار حسابداری</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
public/manifest.json
Normal file
15
public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"short_name": "حسابداری",
|
||||||
|
"name": "نرمافزار حسابداری ساده",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
32
src/App.css
Normal file
32
src/App.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* Additional custom styles for the app */
|
||||||
|
.nav-link {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for form elements */
|
||||||
|
.form-input:focus {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
82
src/App.js
Normal file
82
src/App.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
||||||
|
import './App.css';
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
|
import PersonManagement from './components/PersonManagement';
|
||||||
|
import ProductManagement from './components/ProductManagement';
|
||||||
|
import PurchaseManagement from './components/PurchaseManagement';
|
||||||
|
import SalesManagement from './components/SalesManagement';
|
||||||
|
import InventoryManagement from './components/InventoryManagement';
|
||||||
|
import ChartOfAccounts from './components/ChartOfAccounts';
|
||||||
|
import FinancialReports from './components/FinancialReports';
|
||||||
|
import InvoiceSystem from './components/InvoiceSystem';
|
||||||
|
import InvoiceDemo from './components/InvoiceDemo';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div className="min-h-screen bg-gray-100 farsi-text">
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-blue-600 shadow-lg">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<h1 className="text-white text-xl font-bold">نرمافزار حسابداری</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4 space-x-reverse">
|
||||||
|
<Link to="/" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
داشبورد
|
||||||
|
</Link>
|
||||||
|
<Link to="/persons" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
تعریف شخص
|
||||||
|
</Link>
|
||||||
|
<Link to="/products" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
تعریف کالا
|
||||||
|
</Link>
|
||||||
|
<Link to="/purchases" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
خرید
|
||||||
|
</Link>
|
||||||
|
<Link to="/sales" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
فروش
|
||||||
|
</Link>
|
||||||
|
<Link to="/inventory" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
انبارداری
|
||||||
|
</Link>
|
||||||
|
<Link to="/chart-of-accounts" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
نمودار حسابها
|
||||||
|
</Link>
|
||||||
|
<Link to="/financial-reports" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
گزارشهای مالی
|
||||||
|
</Link>
|
||||||
|
<Link to="/invoices" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
فاکتور
|
||||||
|
</Link>
|
||||||
|
<Link to="/invoice-demo" className="text-white hover:text-gray-200 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
نمونه PDF
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-7xl mx-auto py-6 px-4">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
<Route path="/persons" element={<PersonManagement />} />
|
||||||
|
<Route path="/products" element={<ProductManagement />} />
|
||||||
|
<Route path="/purchases" element={<PurchaseManagement />} />
|
||||||
|
<Route path="/sales" element={<SalesManagement />} />
|
||||||
|
<Route path="/inventory" element={<InventoryManagement />} />
|
||||||
|
<Route path="/chart-of-accounts" element={<ChartOfAccounts />} />
|
||||||
|
<Route path="/financial-reports" element={<FinancialReports />} />
|
||||||
|
<Route path="/invoices" element={<InvoiceSystem />} />
|
||||||
|
<Route path="/invoice-demo" element={<InvoiceDemo />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
276
src/components/ChartOfAccounts.js
Normal file
276
src/components/ChartOfAccounts.js
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const ChartOfAccounts = () => {
|
||||||
|
const [accounts, setAccounts] = useState([
|
||||||
|
{ id: 1, code: '1000', name: 'داراییها', type: 'دارایی', parentId: null, level: 1 },
|
||||||
|
{ id: 2, code: '1100', name: 'داراییهای جاری', type: 'دارایی', parentId: 1, level: 2 },
|
||||||
|
{ id: 3, code: '1110', name: 'موجودی نقد', type: 'دارایی', parentId: 2, level: 3 },
|
||||||
|
{ id: 4, code: '1111', name: 'صندوق', type: 'دارایی', parentId: 3, level: 4, balance: 5000000 },
|
||||||
|
{ id: 5, code: '1112', name: 'بانک ملی', type: 'دارایی', parentId: 3, level: 4, balance: 25000000 },
|
||||||
|
{ id: 6, code: '1120', name: 'حسابهای دریافتنی', type: 'دارایی', parentId: 2, level: 3 },
|
||||||
|
{ id: 7, code: '1121', name: 'مشتریان', type: 'دارایی', parentId: 6, level: 4, balance: 15000000 },
|
||||||
|
{ id: 8, code: '1200', name: 'موجودی کالا', type: 'دارایی', parentId: 2, level: 3, balance: 30000000 },
|
||||||
|
{ id: 9, code: '2000', name: 'بدهیها', type: 'بدهی', parentId: null, level: 1 },
|
||||||
|
{ id: 10, code: '2100', name: 'بدهیهای جاری', type: 'بدهی', parentId: 9, level: 2 },
|
||||||
|
{ id: 11, code: '2110', name: 'حسابهای پرداختنی', type: 'بدهی', parentId: 10, level: 3 },
|
||||||
|
{ id: 12, code: '2111', name: 'تامینکنندگان', type: 'بدهی', parentId: 11, level: 4, balance: 8000000 },
|
||||||
|
{ id: 13, code: '3000', name: 'حقوق صاحبان سهام', type: 'حقوق صاحبان سهام', parentId: null, level: 1 },
|
||||||
|
{ id: 14, code: '3100', name: 'سرمایه', type: 'حقوق صاحبان سهام', parentId: 13, level: 2, balance: 100000000 },
|
||||||
|
{ id: 15, code: '4000', name: 'درآمدها', type: 'درآمد', parentId: null, level: 1 },
|
||||||
|
{ id: 16, code: '4100', name: 'فروش کالا', type: 'درآمد', parentId: 15, level: 2, balance: 50000000 },
|
||||||
|
{ id: 17, code: '5000', name: 'هزینهها', type: 'هزینه', parentId: null, level: 1 },
|
||||||
|
{ id: 18, code: '5100', name: 'هزینه خرید کالا', type: 'هزینه', parentId: 17, level: 2, balance: 30000000 },
|
||||||
|
{ id: 19, code: '5200', name: 'هزینههای عملیاتی', type: 'هزینه', parentId: 17, level: 2 },
|
||||||
|
{ id: 20, code: '5210', name: 'اجاره', type: 'هزینه', parentId: 19, level: 3, balance: 2000000 },
|
||||||
|
{ id: 21, code: '5220', name: 'حقوق و دستمزد', type: 'هزینه', parentId: 19, level: 3, balance: 5000000 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingAccount, setEditingAccount] = useState(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
type: 'دارایی',
|
||||||
|
parentId: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountTypes = ['دارایی', 'بدهی', 'حقوق صاحبان سهام', 'درآمد', 'هزینه'];
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (editingAccount) {
|
||||||
|
// Update existing account
|
||||||
|
setAccounts(accounts.map(account =>
|
||||||
|
account.id === editingAccount.id
|
||||||
|
? {
|
||||||
|
...account,
|
||||||
|
...formData,
|
||||||
|
level: formData.parentId ? accounts.find(a => a.id === parseInt(formData.parentId))?.level + 1 || 1 : 1
|
||||||
|
}
|
||||||
|
: account
|
||||||
|
));
|
||||||
|
setEditingAccount(null);
|
||||||
|
} else {
|
||||||
|
// Add new account
|
||||||
|
const newAccount = {
|
||||||
|
id: Date.now(),
|
||||||
|
...formData,
|
||||||
|
parentId: formData.parentId ? parseInt(formData.parentId) : null,
|
||||||
|
level: formData.parentId ? accounts.find(a => a.id === parseInt(formData.parentId))?.level + 1 || 1 : 1,
|
||||||
|
balance: 0
|
||||||
|
};
|
||||||
|
setAccounts([...accounts, newAccount]);
|
||||||
|
}
|
||||||
|
setFormData({ code: '', name: '', type: 'دارایی', parentId: null });
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (account) => {
|
||||||
|
setEditingAccount(account);
|
||||||
|
setFormData({
|
||||||
|
code: account.code,
|
||||||
|
name: account.name,
|
||||||
|
type: account.type,
|
||||||
|
parentId: account.parentId
|
||||||
|
});
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('آیا از حذف این حساب اطمینان دارید؟')) {
|
||||||
|
// Also delete child accounts
|
||||||
|
const deleteAccountAndChildren = (accountId) => {
|
||||||
|
const children = accounts.filter(acc => acc.parentId === accountId);
|
||||||
|
children.forEach(child => deleteAccountAndChildren(child.id));
|
||||||
|
setAccounts(accounts.filter(account => account.id !== accountId));
|
||||||
|
};
|
||||||
|
deleteAccountAndChildren(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingAccount(null);
|
||||||
|
setFormData({ code: '', name: '', type: 'دارایی', parentId: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price || 0) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccountTypeColor = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'دارایی':
|
||||||
|
return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'بدهی':
|
||||||
|
return 'bg-red-100 text-red-800';
|
||||||
|
case 'حقوق صاحبان سهام':
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
case 'درآمد':
|
||||||
|
return 'bg-purple-100 text-purple-800';
|
||||||
|
case 'هزینه':
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAccountTree = (parentId = null, level = 0) => {
|
||||||
|
const children = accounts.filter(acc => acc.parentId === parentId);
|
||||||
|
return children.map(account => (
|
||||||
|
<React.Fragment key={account.id}>
|
||||||
|
<tr className={`${level > 0 ? 'bg-gray-50' : ''}`}>
|
||||||
|
<td className="pr-4" style={{ paddingRight: `${level * 20}px` }}>
|
||||||
|
{account.code}
|
||||||
|
</td>
|
||||||
|
<td style={{ paddingRight: `${level * 20}px` }}>
|
||||||
|
{level > 0 && '└─ '}{account.name}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs ${getAccountTypeColor(account.type)}`}>
|
||||||
|
{account.type}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-left font-bold">
|
||||||
|
{formatPrice(account.balance)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(account)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(account.id)}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{renderAccountTree(account.id, level + 1)}
|
||||||
|
</React.Fragment>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">نمودار حسابها</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
افزودن حساب جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{editingAccount ? 'ویرایش حساب' : 'افزودن حساب جدید'}
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">کد حساب</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="code"
|
||||||
|
value={formData.code}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
placeholder="مثال: 1111"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">نام حساب</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">نوع حساب</label>
|
||||||
|
<select
|
||||||
|
name="type"
|
||||||
|
value={formData.type}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
{accountTypes.map(type => (
|
||||||
|
<option key={type} value={type}>{type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">حساب والد (اختیاری)</label>
|
||||||
|
<select
|
||||||
|
name="parentId"
|
||||||
|
value={formData.parentId || ''}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="">بدون والد</option>
|
||||||
|
{accounts.map(account => (
|
||||||
|
<option key={account.id} value={account.id}>
|
||||||
|
{account.code} - {account.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button type="submit" className="btn-success flex-1">
|
||||||
|
{editingAccount ? 'ویرایش' : 'افزودن'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleCancel} className="btn-secondary flex-1">
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Accounts Tree Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>کد حساب</th>
|
||||||
|
<th>نام حساب</th>
|
||||||
|
<th>نوع</th>
|
||||||
|
<th>مانده</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{renderAccountTree()}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChartOfAccounts;
|
||||||
150
src/components/Dashboard.js
Normal file
150
src/components/Dashboard.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
title: 'کل اشخاص',
|
||||||
|
value: '25',
|
||||||
|
color: 'bg-blue-500',
|
||||||
|
icon: '👥',
|
||||||
|
link: '/persons'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'کل کالاها',
|
||||||
|
value: '150',
|
||||||
|
color: 'bg-green-500',
|
||||||
|
icon: '📦',
|
||||||
|
link: '/products'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'خرید امروز',
|
||||||
|
value: '5',
|
||||||
|
color: 'bg-yellow-500',
|
||||||
|
icon: '🛒',
|
||||||
|
link: '/purchases'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'فروش امروز',
|
||||||
|
value: '12',
|
||||||
|
color: 'bg-purple-500',
|
||||||
|
icon: '💰',
|
||||||
|
link: '/sales'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'حسابهای فعال',
|
||||||
|
value: '21',
|
||||||
|
color: 'bg-indigo-500',
|
||||||
|
icon: '📊',
|
||||||
|
link: '/chart-of-accounts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'فاکتورهای صادر شده',
|
||||||
|
value: '8',
|
||||||
|
color: 'bg-pink-500',
|
||||||
|
icon: '📄',
|
||||||
|
link: '/invoices'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-8">داشبورد</h1>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6 mb-8">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<Link
|
||||||
|
key={index}
|
||||||
|
to={stat.link}
|
||||||
|
className="card hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`${stat.color} rounded-full p-3 text-white text-2xl`}>
|
||||||
|
{stat.icon}
|
||||||
|
</div>
|
||||||
|
<div className="mr-4">
|
||||||
|
<p className="text-sm font-medium text-gray-600">{stat.title}</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stat.value}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-4">عملیات سریع</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Link
|
||||||
|
to="/purchases"
|
||||||
|
className="flex items-center p-3 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-2xl ml-3">🛒</span>
|
||||||
|
<span className="font-medium">ثبت خرید جدید</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/sales"
|
||||||
|
className="flex items-center p-3 bg-green-50 rounded-lg hover:bg-green-100 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-2xl ml-3">💰</span>
|
||||||
|
<span className="font-medium">ثبت فروش جدید</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/persons"
|
||||||
|
className="flex items-center p-3 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-2xl ml-3">👥</span>
|
||||||
|
<span className="font-medium">افزودن شخص جدید</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/products"
|
||||||
|
className="flex items-center p-3 bg-yellow-50 rounded-lg hover:bg-yellow-100 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-2xl ml-3">📦</span>
|
||||||
|
<span className="font-medium">افزودن کالای جدید</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/invoices"
|
||||||
|
className="flex items-center p-3 bg-pink-50 rounded-lg hover:bg-pink-100 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-2xl ml-3">📄</span>
|
||||||
|
<span className="font-medium">ایجاد فاکتور جدید</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/financial-reports"
|
||||||
|
className="flex items-center p-3 bg-indigo-50 rounded-lg hover:bg-indigo-100 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-2xl ml-3">📊</span>
|
||||||
|
<span className="font-medium">مشاهده گزارشهای مالی</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-4">آخرین فعالیتها</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-green-600 ml-3">✓</span>
|
||||||
|
<span className="text-sm">فروش کالای A به مشتری B</span>
|
||||||
|
<span className="text-xs text-gray-500 mr-auto">2 ساعت پیش</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-blue-600 ml-3">+</span>
|
||||||
|
<span className="text-sm">خرید کالای C از تامینکننده D</span>
|
||||||
|
<span className="text-xs text-gray-500 mr-auto">4 ساعت پیش</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-purple-600 ml-3">👤</span>
|
||||||
|
<span className="text-sm">افزودن مشتری جدید</span>
|
||||||
|
<span className="text-xs text-gray-500 mr-auto">6 ساعت پیش</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
335
src/components/FinancialReports.js
Normal file
335
src/components/FinancialReports.js
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const FinancialReports = () => {
|
||||||
|
const [selectedReport, setSelectedReport] = useState('balance-sheet');
|
||||||
|
const [reportDate, setReportDate] = useState(new Date().toLocaleDateString('fa-IR'));
|
||||||
|
|
||||||
|
// Sample data for reports
|
||||||
|
const balanceSheetData = {
|
||||||
|
assets: [
|
||||||
|
{ name: 'موجودی نقد', amount: 30000000 },
|
||||||
|
{ name: 'حسابهای دریافتنی', amount: 15000000 },
|
||||||
|
{ name: 'موجودی کالا', amount: 30000000 },
|
||||||
|
{ name: 'داراییهای ثابت', amount: 50000000 }
|
||||||
|
],
|
||||||
|
liabilities: [
|
||||||
|
{ name: 'حسابهای پرداختنی', amount: 8000000 },
|
||||||
|
{ name: 'وامهای کوتاهمدت', amount: 12000000 }
|
||||||
|
],
|
||||||
|
equity: [
|
||||||
|
{ name: 'سرمایه', amount: 100000000 },
|
||||||
|
{ name: 'سود انباشته', amount: 15000000 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const profitLossData = {
|
||||||
|
revenues: [
|
||||||
|
{ name: 'فروش کالا', amount: 50000000 },
|
||||||
|
{ name: 'درآمد خدمات', amount: 5000000 }
|
||||||
|
],
|
||||||
|
expenses: [
|
||||||
|
{ name: 'هزینه خرید کالا', amount: 30000000 },
|
||||||
|
{ name: 'هزینههای عملیاتی', amount: 7000000 },
|
||||||
|
{ name: 'اجاره', amount: 2000000 },
|
||||||
|
{ name: 'حقوق و دستمزد', amount: 5000000 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotal = (items) => {
|
||||||
|
return items.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalAssets = calculateTotal(balanceSheetData.assets);
|
||||||
|
const totalLiabilities = calculateTotal(balanceSheetData.liabilities);
|
||||||
|
const totalEquity = calculateTotal(balanceSheetData.equity);
|
||||||
|
const totalRevenues = calculateTotal(profitLossData.revenues);
|
||||||
|
const totalExpenses = calculateTotal(profitLossData.expenses);
|
||||||
|
const netProfit = totalRevenues - totalExpenses;
|
||||||
|
|
||||||
|
const renderBalanceSheet = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold">ترازنامه</h2>
|
||||||
|
<p className="text-gray-600">تاریخ: {reportDate}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* Assets */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-xl font-bold mb-4 text-blue-800">داراییها</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{balanceSheetData.assets.map((asset, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||||
|
<span>{asset.name}</span>
|
||||||
|
<span className="font-bold">{formatPrice(asset.amount)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-between items-center py-2 border-t-2 border-blue-500 font-bold text-lg">
|
||||||
|
<span>مجموع داراییها</span>
|
||||||
|
<span className="text-blue-600">{formatPrice(totalAssets)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liabilities & Equity */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-xl font-bold mb-4 text-red-800">بدهیها</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{balanceSheetData.liabilities.map((liability, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||||
|
<span>{liability.name}</span>
|
||||||
|
<span className="font-bold">{formatPrice(liability.amount)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-between items-center py-2 border-t-2 border-red-500 font-bold">
|
||||||
|
<span>مجموع بدهیها</span>
|
||||||
|
<span className="text-red-600">{formatPrice(totalLiabilities)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-xl font-bold mb-4 text-green-800">حقوق صاحبان سهام</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{balanceSheetData.equity.map((equity, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||||
|
<span>{equity.name}</span>
|
||||||
|
<span className="font-bold">{formatPrice(equity.amount)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-between items-center py-2 border-t-2 border-green-500 font-bold">
|
||||||
|
<span>مجموع حقوق صاحبان سهام</span>
|
||||||
|
<span className="text-green-600">{formatPrice(totalEquity)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Balance Check */}
|
||||||
|
<div className="card bg-gray-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-bold mb-2">بررسی تراز</h3>
|
||||||
|
<p className={`text-lg ${totalAssets === (totalLiabilities + totalEquity) ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{totalAssets === (totalLiabilities + totalEquity) ? '✓ ترازنامه متعادل است' : '✗ ترازنامه نامتعادل است'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
داراییها: {formatPrice(totalAssets)} | بدهیها + حقوق صاحبان سهام: {formatPrice(totalLiabilities + totalEquity)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderProfitLoss = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold">صورت سود و زیان</h2>
|
||||||
|
<p className="text-gray-600">تاریخ: {reportDate}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* Revenues */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-xl font-bold mb-4 text-green-800">درآمدها</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{profitLossData.revenues.map((revenue, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||||
|
<span>{revenue.name}</span>
|
||||||
|
<span className="font-bold">{formatPrice(revenue.amount)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-between items-center py-2 border-t-2 border-green-500 font-bold text-lg">
|
||||||
|
<span>مجموع درآمدها</span>
|
||||||
|
<span className="text-green-600">{formatPrice(totalRevenues)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expenses */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-xl font-bold mb-4 text-red-800">هزینهها</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{profitLossData.expenses.map((expense, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||||
|
<span>{expense.name}</span>
|
||||||
|
<span className="font-bold">{formatPrice(expense.amount)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-between items-center py-2 border-t-2 border-red-500 font-bold text-lg">
|
||||||
|
<span>مجموع هزینهها</span>
|
||||||
|
<span className="text-red-600">{formatPrice(totalExpenses)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Net Profit/Loss */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-xl font-bold mb-4">نتیجه فعالیت</h3>
|
||||||
|
<div className={`text-3xl font-bold ${netProfit >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{netProfit >= 0 ? 'سود خالص' : 'زیان خالص'}: {formatPrice(Math.abs(netProfit))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div className="bg-green-50 p-3 rounded">
|
||||||
|
<p className="font-bold text-green-800">درآمد کل</p>
|
||||||
|
<p className="text-green-600">{formatPrice(totalRevenues)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-red-50 p-3 rounded">
|
||||||
|
<p className="font-bold text-red-800">هزینه کل</p>
|
||||||
|
<p className="text-red-600">{formatPrice(totalExpenses)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTrialBalance = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold">تراز آزمایشی</h2>
|
||||||
|
<p className="text-gray-600">تاریخ: {reportDate}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>کد حساب</th>
|
||||||
|
<th>نام حساب</th>
|
||||||
|
<th>بدهکار</th>
|
||||||
|
<th>بستانکار</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>1111</td>
|
||||||
|
<td>صندوق</td>
|
||||||
|
<td className="text-left">{formatPrice(5000000)}</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>1112</td>
|
||||||
|
<td>بانک ملی</td>
|
||||||
|
<td className="text-left">{formatPrice(25000000)}</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>1121</td>
|
||||||
|
<td>مشتریان</td>
|
||||||
|
<td className="text-left">{formatPrice(15000000)}</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>1200</td>
|
||||||
|
<td>موجودی کالا</td>
|
||||||
|
<td className="text-left">{formatPrice(30000000)}</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>2111</td>
|
||||||
|
<td>تامینکنندگان</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
<td className="text-left">{formatPrice(8000000)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>3100</td>
|
||||||
|
<td>سرمایه</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
<td className="text-left">{formatPrice(100000000)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>4100</td>
|
||||||
|
<td>فروش کالا</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
<td className="text-left">{formatPrice(50000000)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>5100</td>
|
||||||
|
<td>هزینه خرید کالا</td>
|
||||||
|
<td className="text-left">{formatPrice(30000000)}</td>
|
||||||
|
<td className="text-left">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-t-2 border-gray-400 font-bold">
|
||||||
|
<td colSpan="2">مجموع</td>
|
||||||
|
<td className="text-left">{formatPrice(75000000)}</td>
|
||||||
|
<td className="text-left">{formatPrice(158000000)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">گزارشهای مالی</h1>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={reportDate}
|
||||||
|
onChange={(e) => setReportDate(e.target.value)}
|
||||||
|
className="form-input w-40"
|
||||||
|
placeholder="تاریخ گزارش"
|
||||||
|
/>
|
||||||
|
<button className="btn-primary">
|
||||||
|
چاپ گزارش
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Report Type Selector */}
|
||||||
|
<div className="card mb-6">
|
||||||
|
<div className="flex space-x-4 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedReport('balance-sheet')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium ${
|
||||||
|
selectedReport === 'balance-sheet'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
ترازنامه
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedReport('profit-loss')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium ${
|
||||||
|
selectedReport === 'profit-loss'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
صورت سود و زیان
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedReport('trial-balance')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium ${
|
||||||
|
selectedReport === 'trial-balance'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
تراز آزمایشی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Report Content */}
|
||||||
|
{selectedReport === 'balance-sheet' && renderBalanceSheet()}
|
||||||
|
{selectedReport === 'profit-loss' && renderProfitLoss()}
|
||||||
|
{selectedReport === 'trial-balance' && renderTrialBalance()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinancialReports;
|
||||||
299
src/components/InventoryManagement.js
Normal file
299
src/components/InventoryManagement.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const InventoryManagement = () => {
|
||||||
|
const [inventory, setInventory] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
product: 'لپتاپ ایسوس',
|
||||||
|
code: 'LAP001',
|
||||||
|
currentStock: 5,
|
||||||
|
minStock: 2,
|
||||||
|
maxStock: 20,
|
||||||
|
unit: 'عدد',
|
||||||
|
lastUpdate: '1403/01/15'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
product: 'موبایل سامسونگ',
|
||||||
|
code: 'MOB002',
|
||||||
|
currentStock: 12,
|
||||||
|
minStock: 5,
|
||||||
|
maxStock: 50,
|
||||||
|
unit: 'عدد',
|
||||||
|
lastUpdate: '1403/01/14'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
product: 'کتاب برنامهنویسی',
|
||||||
|
code: 'BOK003',
|
||||||
|
currentStock: 25,
|
||||||
|
minStock: 10,
|
||||||
|
maxStock: 100,
|
||||||
|
unit: 'جلد',
|
||||||
|
lastUpdate: '1403/01/13'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showAdjustmentForm, setShowAdjustmentForm] = useState(false);
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState(null);
|
||||||
|
const [adjustmentData, setAdjustmentData] = useState({
|
||||||
|
type: 'افزایش',
|
||||||
|
quantity: '',
|
||||||
|
reason: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdjustmentChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setAdjustmentData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStockAdjustment = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const adjustmentQuantity = parseInt(adjustmentData.quantity);
|
||||||
|
const newStock = adjustmentData.type === 'افزایش'
|
||||||
|
? selectedProduct.currentStock + adjustmentQuantity
|
||||||
|
: selectedProduct.currentStock - adjustmentQuantity;
|
||||||
|
|
||||||
|
if (newStock < 0) {
|
||||||
|
alert('موجودی نمیتواند منفی باشد!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInventory(inventory.map(item =>
|
||||||
|
item.id === selectedProduct.id
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
currentStock: newStock,
|
||||||
|
lastUpdate: new Date().toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
));
|
||||||
|
|
||||||
|
setShowAdjustmentForm(false);
|
||||||
|
setSelectedProduct(null);
|
||||||
|
setAdjustmentData({ type: 'افزایش', quantity: '', reason: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelAdjustment = () => {
|
||||||
|
setShowAdjustmentForm(false);
|
||||||
|
setSelectedProduct(null);
|
||||||
|
setAdjustmentData({ type: 'افزایش', quantity: '', reason: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStockStatus = (current, min, max) => {
|
||||||
|
if (current <= min) return { status: 'کم', color: 'bg-red-100 text-red-800' };
|
||||||
|
if (current >= max) return { status: 'زیاد', color: 'bg-yellow-100 text-yellow-800' };
|
||||||
|
return { status: 'مناسب', color: 'bg-green-100 text-green-800' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const lowStockItems = inventory.filter(item => item.currentStock <= item.minStock);
|
||||||
|
const totalValue = inventory.reduce((sum, item) => {
|
||||||
|
// Assuming average price for calculation
|
||||||
|
const avgPrice = item.product === 'لپتاپ ایسوس' ? 15000000 :
|
||||||
|
item.product === 'موبایل سامسونگ' ? 8000000 : 150000;
|
||||||
|
return sum + (item.currentStock * avgPrice);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">مدیریت انبار</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdjustmentForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
تنظیم موجودی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Inventory Summary */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-blue-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
📦
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">کل کالاها</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{inventory.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-red-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">کمبود موجودی</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{lowStockItems.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-green-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
💰
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">ارزش کل انبار</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900">{formatPrice(totalValue)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-purple-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
📊
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">کل موجودی</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">
|
||||||
|
{inventory.reduce((sum, item) => sum + item.currentStock, 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Low Stock Alert */}
|
||||||
|
{lowStockItems.length > 0 && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||||
|
<h3 className="text-red-800 font-bold mb-2">هشدار کمبود موجودی</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{lowStockItems.map(item => (
|
||||||
|
<div key={item.id} className="text-red-700">
|
||||||
|
{item.product} - موجودی فعلی: {item.currentStock} {item.unit} (حداقل: {item.minStock})
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stock Adjustment Form Modal */}
|
||||||
|
{showAdjustmentForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">تنظیم موجودی</h2>
|
||||||
|
<form onSubmit={handleStockAdjustment} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">کالا</label>
|
||||||
|
<select
|
||||||
|
value={selectedProduct?.id || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const product = inventory.find(item => item.id === parseInt(e.target.value));
|
||||||
|
setSelectedProduct(product);
|
||||||
|
}}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">انتخاب کنید</option>
|
||||||
|
{inventory.map(item => (
|
||||||
|
<option key={item.id} value={item.id}>
|
||||||
|
{item.product} (موجودی فعلی: {item.currentStock})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">نوع تنظیم</label>
|
||||||
|
<select
|
||||||
|
name="type"
|
||||||
|
value={adjustmentData.type}
|
||||||
|
onChange={handleAdjustmentChange}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="افزایش">افزایش موجودی</option>
|
||||||
|
<option value="کاهش">کاهش موجودی</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">تعداد</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="quantity"
|
||||||
|
value={adjustmentData.quantity}
|
||||||
|
onChange={handleAdjustmentChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">دلیل</label>
|
||||||
|
<textarea
|
||||||
|
name="reason"
|
||||||
|
value={adjustmentData.reason}
|
||||||
|
onChange={handleAdjustmentChange}
|
||||||
|
className="form-input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="دلیل تنظیم موجودی..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button type="submit" className="btn-success flex-1">
|
||||||
|
اعمال تغییرات
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleCancelAdjustment} className="btn-secondary flex-1">
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Inventory Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>نام کالا</th>
|
||||||
|
<th>کد کالا</th>
|
||||||
|
<th>موجودی فعلی</th>
|
||||||
|
<th>حداقل</th>
|
||||||
|
<th>حداکثر</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>آخرین بروزرسانی</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{inventory.map(item => {
|
||||||
|
const stockStatus = getStockStatus(item.currentStock, item.minStock, item.maxStock);
|
||||||
|
return (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{item.product}</td>
|
||||||
|
<td>
|
||||||
|
<span className="bg-gray-100 px-2 py-1 rounded text-sm">
|
||||||
|
{item.code}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="font-bold">{item.currentStock} {item.unit}</td>
|
||||||
|
<td>{item.minStock} {item.unit}</td>
|
||||||
|
<td>{item.maxStock} {item.unit}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs ${stockStatus.color}`}>
|
||||||
|
{stockStatus.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{item.lastUpdate}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InventoryManagement;
|
||||||
136
src/components/InvoiceDemo.js
Normal file
136
src/components/InvoiceDemo.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import InvoicePDF from './InvoicePDF';
|
||||||
|
import { sampleInvoice, sampleInvoice2 } from '../data/sampleInvoice';
|
||||||
|
|
||||||
|
const InvoiceDemo = () => {
|
||||||
|
const handleGenerateSamplePDF = (invoice) => {
|
||||||
|
InvoicePDF.generatePDF(invoice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateSampleHTML = (invoice) => {
|
||||||
|
const invoiceHTML = InvoicePDF.generateHTMLInvoice(invoice);
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
printWindow.document.write(invoiceHTML);
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.focus();
|
||||||
|
printWindow.print();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text p-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-8">نمونه فاکتورهای PDF</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
{/* Sample Invoice 1 */}
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-xl font-bold mb-4">فاکتور نمونه 1 - فروش تجهیزات کامپیوتری</h2>
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<p><strong>شماره فاکتور:</strong> {sampleInvoice.invoiceNumber}</p>
|
||||||
|
<p><strong>مشتری:</strong> {sampleInvoice.customer}</p>
|
||||||
|
<p><strong>تعداد اقلام:</strong> {sampleInvoice.items.length}</p>
|
||||||
|
<p><strong>مبلغ کل:</strong> {new Intl.NumberFormat('fa-IR').format(sampleInvoice.total)} ریال</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleGenerateSamplePDF(sampleInvoice)}
|
||||||
|
className="btn-success flex-1"
|
||||||
|
>
|
||||||
|
دانلود PDF
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGenerateSampleHTML(sampleInvoice)}
|
||||||
|
className="btn-primary flex-1"
|
||||||
|
>
|
||||||
|
چاپ HTML
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sample Invoice 2 */}
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-xl font-bold mb-4">فاکتور نمونه 2 - فروش تجهیزات شبکه</h2>
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<p><strong>شماره فاکتور:</strong> {sampleInvoice2.invoiceNumber}</p>
|
||||||
|
<p><strong>مشتری:</strong> {sampleInvoice2.customer}</p>
|
||||||
|
<p><strong>تعداد اقلام:</strong> {sampleInvoice2.items.length}</p>
|
||||||
|
<p><strong>مبلغ کل:</strong> {new Intl.NumberFormat('fa-IR').format(sampleInvoice2.total)} ریال</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleGenerateSamplePDF(sampleInvoice2)}
|
||||||
|
className="btn-success flex-1"
|
||||||
|
>
|
||||||
|
دانلود PDF
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGenerateSampleHTML(sampleInvoice2)}
|
||||||
|
className="btn-primary flex-1"
|
||||||
|
>
|
||||||
|
چاپ HTML
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<div className="card mt-8">
|
||||||
|
<h2 className="text-xl font-bold mb-4">راهنمای استفاده</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start space-x-3 space-x-reverse">
|
||||||
|
<span className="bg-green-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold">1</span>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">دانلود PDF:</p>
|
||||||
|
<p className="text-gray-600">فایل PDF فاکتور را مستقیماً دانلود میکند</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3 space-x-reverse">
|
||||||
|
<span className="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold">2</span>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">چاپ HTML:</p>
|
||||||
|
<p className="text-gray-600">فاکتور را در پنجره جدید باز کرده و امکان چاپ فراهم میکند</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3 space-x-reverse">
|
||||||
|
<span className="bg-purple-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold">3</span>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">پیشنمایش:</p>
|
||||||
|
<p className="text-gray-600">در بخش فاکتورها، دکمه "پیشنمایش" را کلیک کنید</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="card mt-8">
|
||||||
|
<h2 className="text-xl font-bold mb-4">ویژگیهای فاکتور PDF</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-bold text-green-600">✅ ویژگیهای موجود:</h3>
|
||||||
|
<ul className="space-y-1 text-sm">
|
||||||
|
<li>• طراحی حرفهای و زیبا</li>
|
||||||
|
<li>• پشتیبانی کامل از زبان فارسی</li>
|
||||||
|
<li>• محاسبه خودکار مالیات</li>
|
||||||
|
<li>• نمایش جزئیات کامل مشتری</li>
|
||||||
|
<li>• جدول اقلام با قیمتگذاری</li>
|
||||||
|
<li>• فرمتبندی اعداد به فارسی</li>
|
||||||
|
<li>• قابلیت چاپ و ذخیره</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-bold text-blue-600">🔧 قابلیتهای فنی:</h3>
|
||||||
|
<ul className="space-y-1 text-sm">
|
||||||
|
<li>• تولید PDF با jsPDF</li>
|
||||||
|
<li>• نسخه HTML برای چاپ</li>
|
||||||
|
<li>• طراحی ریسپانسیو</li>
|
||||||
|
<li>• پشتیبانی از چندین قلم</li>
|
||||||
|
<li>• بهینهسازی برای چاپ</li>
|
||||||
|
<li>• سازگار با مرورگرهای مختلف</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvoiceDemo;
|
||||||
257
src/components/InvoicePDF.js
Normal file
257
src/components/InvoicePDF.js
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import jsPDF from 'jspdf';
|
||||||
|
|
||||||
|
const InvoicePDF = {
|
||||||
|
generatePDF: (invoice) => {
|
||||||
|
const doc = new jsPDF();
|
||||||
|
|
||||||
|
// Set font for Persian text (using a basic approach)
|
||||||
|
doc.setFont('helvetica');
|
||||||
|
|
||||||
|
// Company Header
|
||||||
|
doc.setFontSize(20);
|
||||||
|
doc.text('شرکت حسابداری نمونه', 105, 20, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.text('آدرس: تهران، خیابان ولیعصر، پلاک 123', 105, 30, { align: 'center' });
|
||||||
|
doc.text('تلفن: 021-12345678 | ایمیل: info@company.com', 105, 35, { align: 'center' });
|
||||||
|
|
||||||
|
// Invoice Title
|
||||||
|
doc.setFontSize(18);
|
||||||
|
doc.text('فاکتور فروش', 105, 50, { align: 'center' });
|
||||||
|
|
||||||
|
// Invoice Details
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.text(`شماره فاکتور: ${invoice.invoiceNumber}`, 20, 65);
|
||||||
|
doc.text(`تاریخ: ${invoice.date}`, 20, 70);
|
||||||
|
doc.text(`وضعیت: ${invoice.status}`, 20, 75);
|
||||||
|
|
||||||
|
// Customer Information
|
||||||
|
doc.setFontSize(14);
|
||||||
|
doc.text('اطلاعات مشتری:', 20, 90);
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.text(`نام: ${invoice.customer}`, 20, 100);
|
||||||
|
doc.text(`آدرس: ${invoice.customerAddress}`, 20, 105);
|
||||||
|
|
||||||
|
// Items Table Header
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.text('شرح کالا/خدمات', 20, 125);
|
||||||
|
doc.text('تعداد', 100, 125);
|
||||||
|
doc.text('قیمت واحد', 120, 125);
|
||||||
|
doc.text('مبلغ', 160, 125);
|
||||||
|
|
||||||
|
// Draw line under header
|
||||||
|
doc.line(20, 130, 190, 130);
|
||||||
|
|
||||||
|
// Items
|
||||||
|
let yPosition = 140;
|
||||||
|
invoice.items.forEach((item, index) => {
|
||||||
|
doc.text(item.product, 20, yPosition);
|
||||||
|
doc.text(item.quantity.toString(), 100, yPosition);
|
||||||
|
doc.text(InvoicePDF.formatPrice(item.unitPrice), 120, yPosition);
|
||||||
|
doc.text(InvoicePDF.formatPrice(item.total), 160, yPosition);
|
||||||
|
yPosition += 10;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw line under items
|
||||||
|
doc.line(20, yPosition, 190, yPosition);
|
||||||
|
|
||||||
|
// Totals
|
||||||
|
yPosition += 15;
|
||||||
|
doc.text('مجموع:', 120, yPosition);
|
||||||
|
doc.text(InvoicePDF.formatPrice(invoice.subtotal), 160, yPosition);
|
||||||
|
|
||||||
|
yPosition += 10;
|
||||||
|
doc.text('مالیات (10%):', 120, yPosition);
|
||||||
|
doc.text(InvoicePDF.formatPrice(invoice.tax), 160, yPosition);
|
||||||
|
|
||||||
|
yPosition += 10;
|
||||||
|
doc.setFontSize(14);
|
||||||
|
doc.text('مبلغ کل:', 120, yPosition);
|
||||||
|
doc.text(InvoicePDF.formatPrice(invoice.total), 160, yPosition);
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
yPosition += 30;
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.text('با تشکر از انتخاب شما', 105, yPosition, { align: 'center' });
|
||||||
|
doc.text('این فاکتور به صورت خودکار تولید شده است', 105, yPosition + 10, { align: 'center' });
|
||||||
|
|
||||||
|
// Save the PDF
|
||||||
|
doc.save(`invoice-${invoice.invoiceNumber}.pdf`);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatPrice: (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Alternative method using HTML to PDF conversion
|
||||||
|
generateHTMLInvoice: (invoice) => {
|
||||||
|
const invoiceHTML = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>فاکتور ${invoice.invoiceNumber}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Vazir', Tahoma, sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.invoice-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 2px solid #333;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.company-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.company-info {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.invoice-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.invoice-details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.invoice-info, .customer-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.invoice-info h3, .customer-info h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.items-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.items-table th, .items-table td {
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.items-table th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.totals {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.total-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
.total-final {
|
||||||
|
border-top: 2px solid #333;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="invoice-container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="company-name">شرکت حسابداری نمونه</div>
|
||||||
|
<div class="company-info">
|
||||||
|
آدرس: تهران، خیابان ولیعصر، پلاک 123<br>
|
||||||
|
تلفن: 021-12345678 | ایمیل: info@company.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="invoice-title">فاکتور فروش</div>
|
||||||
|
|
||||||
|
<div class="invoice-details">
|
||||||
|
<div class="invoice-info">
|
||||||
|
<h3>اطلاعات فاکتور</h3>
|
||||||
|
<p><strong>شماره فاکتور:</strong> ${invoice.invoiceNumber}</p>
|
||||||
|
<p><strong>تاریخ:</strong> ${invoice.date}</p>
|
||||||
|
<p><strong>وضعیت:</strong> ${invoice.status}</p>
|
||||||
|
</div>
|
||||||
|
<div class="customer-info">
|
||||||
|
<h3>اطلاعات مشتری</h3>
|
||||||
|
<p><strong>نام:</strong> ${invoice.customer}</p>
|
||||||
|
<p><strong>آدرس:</strong> ${invoice.customerAddress}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="items-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>شرح کالا/خدمات</th>
|
||||||
|
<th>تعداد</th>
|
||||||
|
<th>قیمت واحد</th>
|
||||||
|
<th>مبلغ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${invoice.items.map(item => `
|
||||||
|
<tr>
|
||||||
|
<td>${item.product}</td>
|
||||||
|
<td>${item.quantity}</td>
|
||||||
|
<td>${InvoicePDF.formatPrice(item.unitPrice)}</td>
|
||||||
|
<td>${InvoicePDF.formatPrice(item.total)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="totals">
|
||||||
|
<div class="total-row">
|
||||||
|
<span>مجموع:</span>
|
||||||
|
<span>${InvoicePDF.formatPrice(invoice.subtotal)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row">
|
||||||
|
<span>مالیات (10%):</span>
|
||||||
|
<span>${InvoicePDF.formatPrice(invoice.tax)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row total-final">
|
||||||
|
<span>مبلغ کل:</span>
|
||||||
|
<span>${InvoicePDF.formatPrice(invoice.total)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>با تشکر از انتخاب شما</p>
|
||||||
|
<p>این فاکتور به صورت خودکار تولید شده است</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return invoiceHTML;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvoicePDF;
|
||||||
150
src/components/InvoicePreview.js
Normal file
150
src/components/InvoicePreview.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const InvoicePreview = ({ invoice, onClose }) => {
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-4xl my-8">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold">پیشنمایش فاکتور</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700 text-2xl"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Invoice Preview */}
|
||||||
|
<div className="border-2 border-gray-300 p-8 bg-white" style={{ minHeight: '800px' }}>
|
||||||
|
{/* Company Header */}
|
||||||
|
<div className="text-center border-b-2 border-gray-400 pb-6 mb-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-4">شرکت حسابداری نمونه</h1>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
<p>آدرس: تهران، خیابان ولیعصر، پلاک 123</p>
|
||||||
|
<p>تلفن: 021-12345678 | ایمیل: info@company.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Invoice Title */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-2xl font-bold">فاکتور فروش</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Invoice Details */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4">اطلاعات فاکتور</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p><strong>شماره فاکتور:</strong> {invoice.invoiceNumber}</p>
|
||||||
|
<p><strong>تاریخ:</strong> {invoice.date}</p>
|
||||||
|
<p><strong>وضعیت:</strong>
|
||||||
|
<span className={`ml-2 px-2 py-1 rounded-full text-xs ${
|
||||||
|
invoice.status === 'پرداخت شده' ? 'bg-green-100 text-green-800' :
|
||||||
|
invoice.status === 'در انتظار پرداخت' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
{invoice.status}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4">اطلاعات مشتری</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p><strong>نام:</strong> {invoice.customer}</p>
|
||||||
|
<p><strong>آدرس:</strong> {invoice.customerAddress}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Items Table */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<table className="w-full border-collapse border border-gray-400">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100">
|
||||||
|
<th className="border border-gray-400 p-3 text-right">شرح کالا/خدمات</th>
|
||||||
|
<th className="border border-gray-400 p-3 text-center">تعداد</th>
|
||||||
|
<th className="border border-gray-400 p-3 text-left">قیمت واحد</th>
|
||||||
|
<th className="border border-gray-400 p-3 text-left">مبلغ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invoice.items.map((item, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className="border border-gray-400 p-3">{item.product}</td>
|
||||||
|
<td className="border border-gray-400 p-3 text-center">{item.quantity}</td>
|
||||||
|
<td className="border border-gray-400 p-3 text-left">{formatPrice(item.unitPrice)}</td>
|
||||||
|
<td className="border border-gray-400 p-3 text-left font-bold">{formatPrice(item.total)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Totals */}
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="flex justify-between items-center py-2 border-b border-gray-300">
|
||||||
|
<span className="font-medium">مجموع:</span>
|
||||||
|
<span className="font-bold">{formatPrice(invoice.subtotal)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-2 border-b border-gray-300">
|
||||||
|
<span className="font-medium">مالیات (10%):</span>
|
||||||
|
<span className="font-bold">{formatPrice(invoice.tax)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-4 border-t-2 border-gray-400">
|
||||||
|
<span className="text-xl font-bold">مبلغ کل:</span>
|
||||||
|
<span className="text-xl font-bold text-blue-600">{formatPrice(invoice.total)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="text-center mt-12 pt-6 border-t border-gray-300">
|
||||||
|
<p className="text-gray-600 mb-2">با تشکر از انتخاب شما</p>
|
||||||
|
<p className="text-sm text-gray-500">این فاکتور به صورت خودکار تولید شده است</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex justify-center space-x-4 space-x-reverse mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Generate PDF
|
||||||
|
const { generatePDF } = require('./InvoicePDF');
|
||||||
|
generatePDF(invoice);
|
||||||
|
}}
|
||||||
|
className="btn-success px-6 py-2"
|
||||||
|
>
|
||||||
|
دانلود PDF
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Generate HTML and print
|
||||||
|
const { generateHTMLInvoice } = require('./InvoicePDF');
|
||||||
|
const invoiceHTML = generateHTMLInvoice(invoice);
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
printWindow.document.write(invoiceHTML);
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.focus();
|
||||||
|
printWindow.print();
|
||||||
|
}}
|
||||||
|
className="btn-primary px-6 py-2"
|
||||||
|
>
|
||||||
|
چاپ فاکتور
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="btn-secondary px-6 py-2"
|
||||||
|
>
|
||||||
|
بستن
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvoicePreview;
|
||||||
516
src/components/InvoiceSystem.js
Normal file
516
src/components/InvoiceSystem.js
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import InvoicePDF from './InvoicePDF';
|
||||||
|
import InvoicePreview from './InvoicePreview';
|
||||||
|
|
||||||
|
const InvoiceSystem = () => {
|
||||||
|
const [invoices, setInvoices] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
invoiceNumber: 'INV-001',
|
||||||
|
date: '1403/01/15',
|
||||||
|
customer: 'احمد محمدی',
|
||||||
|
customerAddress: 'تهران، خیابان ولیعصر',
|
||||||
|
items: [
|
||||||
|
{ product: 'لپتاپ ایسوس', quantity: 1, unitPrice: 16000000, total: 16000000 },
|
||||||
|
{ product: 'ماوس بیسیم', quantity: 1, unitPrice: 500000, total: 500000 }
|
||||||
|
],
|
||||||
|
subtotal: 16500000,
|
||||||
|
tax: 1650000,
|
||||||
|
total: 18150000,
|
||||||
|
status: 'پرداخت شده'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
invoiceNumber: 'INV-002',
|
||||||
|
date: '1403/01/14',
|
||||||
|
customer: 'فاطمه احمدی',
|
||||||
|
customerAddress: 'اصفهان، خیابان چهارباغ',
|
||||||
|
items: [
|
||||||
|
{ product: 'موبایل سامسونگ', quantity: 2, unitPrice: 8500000, total: 17000000 }
|
||||||
|
],
|
||||||
|
subtotal: 17000000,
|
||||||
|
tax: 1700000,
|
||||||
|
total: 18700000,
|
||||||
|
status: 'در انتظار پرداخت'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingInvoice, setEditingInvoice] = useState(null);
|
||||||
|
const [previewInvoice, setPreviewInvoice] = useState(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
customer: '',
|
||||||
|
customerAddress: '',
|
||||||
|
items: [{ product: '', quantity: '', unitPrice: '', total: 0 }],
|
||||||
|
tax: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
const customers = ['احمد محمدی', 'فاطمه احمدی', 'علی رضایی'];
|
||||||
|
const products = ['لپتاپ ایسوس', 'موبایل سامسونگ', 'کتاب برنامهنویسی', 'ماوس بیسیم'];
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemChange = (index, field, value) => {
|
||||||
|
const newItems = [...formData.items];
|
||||||
|
newItems[index] = { ...newItems[index], [field]: value };
|
||||||
|
|
||||||
|
if (field === 'quantity' || field === 'unitPrice') {
|
||||||
|
const quantity = parseFloat(field === 'quantity' ? value : newItems[index].quantity) || 0;
|
||||||
|
const unitPrice = parseFloat(field === 'unitPrice' ? value : newItems[index].unitPrice) || 0;
|
||||||
|
newItems[index].total = quantity * unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData(prev => ({ ...prev, items: newItems }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addItem = () => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
items: [...prev.items, { product: '', quantity: '', unitPrice: '', total: 0 }]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeItem = (index) => {
|
||||||
|
if (formData.items.length > 1) {
|
||||||
|
const newItems = formData.items.filter((_, i) => i !== index);
|
||||||
|
setFormData(prev => ({ ...prev, items: newItems }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotals = () => {
|
||||||
|
const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0);
|
||||||
|
const tax = (subtotal * formData.tax) / 100;
|
||||||
|
const total = subtotal + tax;
|
||||||
|
return { subtotal, tax, total };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { subtotal, tax, total } = calculateTotals();
|
||||||
|
const invoiceNumber = `INV-${String(invoices.length + 1).padStart(3, '0')}`;
|
||||||
|
|
||||||
|
if (editingInvoice) {
|
||||||
|
// Update existing invoice
|
||||||
|
setInvoices(invoices.map(invoice =>
|
||||||
|
invoice.id === editingInvoice.id
|
||||||
|
? {
|
||||||
|
...invoice,
|
||||||
|
...formData,
|
||||||
|
subtotal,
|
||||||
|
tax,
|
||||||
|
total,
|
||||||
|
items: formData.items.filter(item => item.product && item.quantity && item.unitPrice)
|
||||||
|
}
|
||||||
|
: invoice
|
||||||
|
));
|
||||||
|
setEditingInvoice(null);
|
||||||
|
} else {
|
||||||
|
// Add new invoice
|
||||||
|
const newInvoice = {
|
||||||
|
id: Date.now(),
|
||||||
|
invoiceNumber,
|
||||||
|
date: new Date().toLocaleDateString('fa-IR'),
|
||||||
|
...formData,
|
||||||
|
subtotal,
|
||||||
|
tax,
|
||||||
|
total,
|
||||||
|
status: 'در انتظار پرداخت',
|
||||||
|
items: formData.items.filter(item => item.product && item.quantity && item.unitPrice)
|
||||||
|
};
|
||||||
|
setInvoices([newInvoice, ...invoices]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
customer: '',
|
||||||
|
customerAddress: '',
|
||||||
|
items: [{ product: '', quantity: '', unitPrice: '', total: 0 }],
|
||||||
|
tax: 10
|
||||||
|
});
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (invoice) => {
|
||||||
|
setEditingInvoice(invoice);
|
||||||
|
setFormData({
|
||||||
|
customer: invoice.customer,
|
||||||
|
customerAddress: invoice.customerAddress,
|
||||||
|
items: invoice.items.length > 0 ? invoice.items : [{ product: '', quantity: '', unitPrice: '', total: 0 }],
|
||||||
|
tax: 10
|
||||||
|
});
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('آیا از حذف این فاکتور اطمینان دارید؟')) {
|
||||||
|
setInvoices(invoices.filter(invoice => invoice.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingInvoice(null);
|
||||||
|
setFormData({
|
||||||
|
customer: '',
|
||||||
|
customerAddress: '',
|
||||||
|
items: [{ product: '', quantity: '', unitPrice: '', total: 0 }],
|
||||||
|
tax: 10
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'پرداخت شده':
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
case 'در انتظار پرداخت':
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case 'لغو شده':
|
||||||
|
return 'bg-red-100 text-red-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrintInvoice = (invoice) => {
|
||||||
|
// Generate PDF using jsPDF
|
||||||
|
InvoicePDF.generatePDF(invoice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrintHTML = (invoice) => {
|
||||||
|
// Generate HTML version and open in new window for printing
|
||||||
|
const invoiceHTML = InvoicePDF.generateHTMLInvoice(invoice);
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
printWindow.document.write(invoiceHTML);
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.focus();
|
||||||
|
printWindow.print();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreviewInvoice = (invoice) => {
|
||||||
|
setPreviewInvoice(invoice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { subtotal, tax, total } = calculateTotals();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">سیستم فاکتور</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
ایجاد فاکتور جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Invoice Summary */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-blue-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
📄
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">کل فاکتورها</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{invoices.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-green-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
💰
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">کل مبلغ</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900">
|
||||||
|
{formatPrice(invoices.reduce((sum, inv) => sum + inv.total, 0))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-yellow-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
⏳
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">در انتظار پرداخت</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">
|
||||||
|
{invoices.filter(inv => inv.status === 'در انتظار پرداخت').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-purple-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
✅
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">پرداخت شده</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">
|
||||||
|
{invoices.filter(inv => inv.status === 'پرداخت شده').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-4xl my-8">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{editingInvoice ? 'ویرایش فاکتور' : 'ایجاد فاکتور جدید'}
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Customer Info */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">مشتری</label>
|
||||||
|
<select
|
||||||
|
name="customer"
|
||||||
|
value={formData.customer}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">انتخاب کنید</option>
|
||||||
|
{customers.map(customer => (
|
||||||
|
<option key={customer} value={customer}>{customer}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">آدرس مشتری</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="customerAddress"
|
||||||
|
value={formData.customerAddress}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Items */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<label className="form-label">اقلام فاکتور</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addItem}
|
||||||
|
className="btn-secondary text-sm"
|
||||||
|
>
|
||||||
|
افزودن قلم
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{formData.items.map((item, index) => (
|
||||||
|
<div key={index} className="grid grid-cols-12 gap-2 items-end">
|
||||||
|
<div className="col-span-5">
|
||||||
|
<select
|
||||||
|
value={item.product}
|
||||||
|
onChange={(e) => handleItemChange(index, 'product', e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">انتخاب کالا</option>
|
||||||
|
{products.map(product => (
|
||||||
|
<option key={product} value={product}>{product}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="تعداد"
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={(e) => handleItemChange(index, 'quantity', e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="قیمت واحد"
|
||||||
|
value={item.unitPrice}
|
||||||
|
onChange={(e) => handleItemChange(index, 'unitPrice', e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formatPrice(item.total)}
|
||||||
|
className="form-input bg-gray-100"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
{formData.items.length > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeItem(index)}
|
||||||
|
className="text-red-600 hover:text-red-800"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tax */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">درصد مالیات</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="tax"
|
||||||
|
value={formData.tax}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">مجموع</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formatPrice(subtotal)}
|
||||||
|
className="form-input bg-gray-100"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">کل با مالیات</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formatPrice(total)}
|
||||||
|
className="form-input bg-gray-100 font-bold"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button type="submit" className="btn-success flex-1">
|
||||||
|
{editingInvoice ? 'ویرایش' : 'ایجاد'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleCancel} className="btn-secondary flex-1">
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Invoices Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>شماره فاکتور</th>
|
||||||
|
<th>تاریخ</th>
|
||||||
|
<th>مشتری</th>
|
||||||
|
<th>تعداد اقلام</th>
|
||||||
|
<th>مبلغ کل</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invoices.map(invoice => (
|
||||||
|
<tr key={invoice.id}>
|
||||||
|
<td>
|
||||||
|
<span className="bg-blue-100 px-2 py-1 rounded text-sm">
|
||||||
|
{invoice.invoiceNumber}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{invoice.date}</td>
|
||||||
|
<td>{invoice.customer}</td>
|
||||||
|
<td>{invoice.items.length}</td>
|
||||||
|
<td className="font-bold">{formatPrice(invoice.total)}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(invoice.status)}`}>
|
||||||
|
{invoice.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(invoice)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(invoice.id)}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePreviewInvoice(invoice)}
|
||||||
|
className="text-indigo-600 hover:text-indigo-800 text-sm"
|
||||||
|
title="پیشنمایش"
|
||||||
|
>
|
||||||
|
پیشنمایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePrintInvoice(invoice)}
|
||||||
|
className="text-green-600 hover:text-green-800 text-sm"
|
||||||
|
title="دانلود PDF"
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePrintHTML(invoice)}
|
||||||
|
className="text-purple-600 hover:text-purple-800 text-sm"
|
||||||
|
title="چاپ"
|
||||||
|
>
|
||||||
|
چاپ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Invoice Preview Modal */}
|
||||||
|
{previewInvoice && (
|
||||||
|
<InvoicePreview
|
||||||
|
invoice={previewInvoice}
|
||||||
|
onClose={() => setPreviewInvoice(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvoiceSystem;
|
||||||
197
src/components/PersonManagement.js
Normal file
197
src/components/PersonManagement.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const PersonManagement = () => {
|
||||||
|
const [persons, setPersons] = useState([
|
||||||
|
{ id: 1, name: 'احمد محمدی', type: 'مشتری', phone: '09123456789', address: 'تهران، خیابان ولیعصر' },
|
||||||
|
{ id: 2, name: 'شرکت پارس', type: 'تامینکننده', phone: '02112345678', address: 'تهران، خیابان آزادی' },
|
||||||
|
{ id: 3, name: 'فاطمه احمدی', type: 'مشتری', phone: '09187654321', address: 'اصفهان، خیابان چهارباغ' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingPerson, setEditingPerson] = useState(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
type: 'مشتری',
|
||||||
|
phone: '',
|
||||||
|
address: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (editingPerson) {
|
||||||
|
// Update existing person
|
||||||
|
setPersons(persons.map(person =>
|
||||||
|
person.id === editingPerson.id
|
||||||
|
? { ...person, ...formData }
|
||||||
|
: person
|
||||||
|
));
|
||||||
|
setEditingPerson(null);
|
||||||
|
} else {
|
||||||
|
// Add new person
|
||||||
|
const newPerson = {
|
||||||
|
id: Date.now(),
|
||||||
|
...formData
|
||||||
|
};
|
||||||
|
setPersons([...persons, newPerson]);
|
||||||
|
}
|
||||||
|
setFormData({ name: '', type: 'مشتری', phone: '', address: '' });
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (person) => {
|
||||||
|
setEditingPerson(person);
|
||||||
|
setFormData(person);
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('آیا از حذف این شخص اطمینان دارید؟')) {
|
||||||
|
setPersons(persons.filter(person => person.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingPerson(null);
|
||||||
|
setFormData({ name: '', type: 'مشتری', phone: '', address: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">مدیریت اشخاص</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
افزودن شخص جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{editingPerson ? 'ویرایش شخص' : 'افزودن شخص جدید'}
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">نام</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">نوع</label>
|
||||||
|
<select
|
||||||
|
name="type"
|
||||||
|
value={formData.type}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="مشتری">مشتری</option>
|
||||||
|
<option value="تامینکننده">تامینکننده</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">شماره تلفن</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">آدرس</label>
|
||||||
|
<textarea
|
||||||
|
name="address"
|
||||||
|
value={formData.address}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button type="submit" className="btn-success flex-1">
|
||||||
|
{editingPerson ? 'ویرایش' : 'افزودن'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleCancel} className="btn-secondary flex-1">
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Persons Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>نام</th>
|
||||||
|
<th>نوع</th>
|
||||||
|
<th>شماره تلفن</th>
|
||||||
|
<th>آدرس</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{persons.map(person => (
|
||||||
|
<tr key={person.id}>
|
||||||
|
<td>{person.name}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||||
|
person.type === 'مشتری'
|
||||||
|
? 'bg-blue-100 text-blue-800'
|
||||||
|
: 'bg-green-100 text-green-800'
|
||||||
|
}`}>
|
||||||
|
{person.type}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{person.phone}</td>
|
||||||
|
<td>{person.address}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(person)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(person.id)}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PersonManagement;
|
||||||
235
src/components/ProductManagement.js
Normal file
235
src/components/ProductManagement.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const ProductManagement = () => {
|
||||||
|
const [products, setProducts] = useState([
|
||||||
|
{ id: 1, name: 'لپتاپ ایسوس', code: 'LAP001', price: 15000000, unit: 'عدد', stock: 5 },
|
||||||
|
{ id: 2, name: 'موبایل سامسونگ', code: 'MOB002', price: 8000000, unit: 'عدد', stock: 12 },
|
||||||
|
{ id: 3, name: 'کتاب برنامهنویسی', code: 'BOK003', price: 150000, unit: 'جلد', stock: 25 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingProduct, setEditingProduct] = useState(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
price: '',
|
||||||
|
unit: 'عدد',
|
||||||
|
stock: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (editingProduct) {
|
||||||
|
// Update existing product
|
||||||
|
setProducts(products.map(product =>
|
||||||
|
product.id === editingProduct.id
|
||||||
|
? { ...product, ...formData, price: parseFloat(formData.price), stock: parseInt(formData.stock) }
|
||||||
|
: product
|
||||||
|
));
|
||||||
|
setEditingProduct(null);
|
||||||
|
} else {
|
||||||
|
// Add new product
|
||||||
|
const newProduct = {
|
||||||
|
id: Date.now(),
|
||||||
|
...formData,
|
||||||
|
price: parseFloat(formData.price),
|
||||||
|
stock: parseInt(formData.stock)
|
||||||
|
};
|
||||||
|
setProducts([...products, newProduct]);
|
||||||
|
}
|
||||||
|
setFormData({ name: '', code: '', price: '', unit: 'عدد', stock: '' });
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (product) => {
|
||||||
|
setEditingProduct(product);
|
||||||
|
setFormData({
|
||||||
|
...product,
|
||||||
|
price: product.price.toString(),
|
||||||
|
stock: product.stock.toString()
|
||||||
|
});
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('آیا از حذف این کالا اطمینان دارید؟')) {
|
||||||
|
setProducts(products.filter(product => product.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingProduct(null);
|
||||||
|
setFormData({ name: '', code: '', price: '', unit: 'عدد', stock: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">مدیریت کالاها</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
افزودن کالای جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{editingProduct ? 'ویرایش کالا' : 'افزودن کالای جدید'}
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">نام کالا</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">کد کالا</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="code"
|
||||||
|
value={formData.code}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">قیمت (ریال)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="price"
|
||||||
|
value={formData.price}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">واحد</label>
|
||||||
|
<select
|
||||||
|
name="unit"
|
||||||
|
value={formData.unit}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="عدد">عدد</option>
|
||||||
|
<option value="کیلوگرم">کیلوگرم</option>
|
||||||
|
<option value="متر">متر</option>
|
||||||
|
<option value="لیتر">لیتر</option>
|
||||||
|
<option value="بسته">بسته</option>
|
||||||
|
<option value="کارتن">کارتن</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">موجودی</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="stock"
|
||||||
|
value={formData.stock}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button type="submit" className="btn-success flex-1">
|
||||||
|
{editingProduct ? 'ویرایش' : 'افزودن'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleCancel} className="btn-secondary flex-1">
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Products Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>نام کالا</th>
|
||||||
|
<th>کد کالا</th>
|
||||||
|
<th>قیمت</th>
|
||||||
|
<th>واحد</th>
|
||||||
|
<th>موجودی</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{products.map(product => (
|
||||||
|
<tr key={product.id}>
|
||||||
|
<td>{product.name}</td>
|
||||||
|
<td>
|
||||||
|
<span className="bg-gray-100 px-2 py-1 rounded text-sm">
|
||||||
|
{product.code}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{formatPrice(product.price)}</td>
|
||||||
|
<td>{product.unit}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||||
|
product.stock > 10
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: product.stock > 0
|
||||||
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
{product.stock}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(product)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(product.id)}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductManagement;
|
||||||
302
src/components/PurchaseManagement.js
Normal file
302
src/components/PurchaseManagement.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const PurchaseManagement = () => {
|
||||||
|
const [purchases, setPurchases] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
date: '1403/01/15',
|
||||||
|
supplier: 'شرکت پارس',
|
||||||
|
product: 'لپتاپ ایسوس',
|
||||||
|
quantity: 2,
|
||||||
|
unitPrice: 15000000,
|
||||||
|
total: 30000000,
|
||||||
|
status: 'تایید شده'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
date: '1403/01/14',
|
||||||
|
supplier: 'تامینکننده الف',
|
||||||
|
product: 'موبایل سامسونگ',
|
||||||
|
quantity: 5,
|
||||||
|
unitPrice: 8000000,
|
||||||
|
total: 40000000,
|
||||||
|
status: 'در انتظار'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingPurchase, setEditingPurchase] = useState(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
date: new Date().toLocaleDateString('fa-IR'),
|
||||||
|
supplier: '',
|
||||||
|
product: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
status: 'در انتظار'
|
||||||
|
});
|
||||||
|
|
||||||
|
const suppliers = ['شرکت پارس', 'تامینکننده الف', 'تامینکننده ب'];
|
||||||
|
const products = ['لپتاپ ایسوس', 'موبایل سامسونگ', 'کتاب برنامهنویسی'];
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const total = parseFloat(formData.quantity) * parseFloat(formData.unitPrice);
|
||||||
|
|
||||||
|
if (editingPurchase) {
|
||||||
|
// Update existing purchase
|
||||||
|
setPurchases(purchases.map(purchase =>
|
||||||
|
purchase.id === editingPurchase.id
|
||||||
|
? {
|
||||||
|
...purchase,
|
||||||
|
...formData,
|
||||||
|
quantity: parseInt(formData.quantity),
|
||||||
|
unitPrice: parseFloat(formData.unitPrice),
|
||||||
|
total: total
|
||||||
|
}
|
||||||
|
: purchase
|
||||||
|
));
|
||||||
|
setEditingPurchase(null);
|
||||||
|
} else {
|
||||||
|
// Add new purchase
|
||||||
|
const newPurchase = {
|
||||||
|
id: Date.now(),
|
||||||
|
...formData,
|
||||||
|
quantity: parseInt(formData.quantity),
|
||||||
|
unitPrice: parseFloat(formData.unitPrice),
|
||||||
|
total: total
|
||||||
|
};
|
||||||
|
setPurchases([newPurchase, ...purchases]);
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
date: new Date().toLocaleDateString('fa-IR'),
|
||||||
|
supplier: '',
|
||||||
|
product: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
status: 'در انتظار'
|
||||||
|
});
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (purchase) => {
|
||||||
|
setEditingPurchase(purchase);
|
||||||
|
setFormData({
|
||||||
|
...purchase,
|
||||||
|
quantity: purchase.quantity.toString(),
|
||||||
|
unitPrice: purchase.unitPrice.toString()
|
||||||
|
});
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('آیا از حذف این خرید اطمینان دارید؟')) {
|
||||||
|
setPurchases(purchases.filter(purchase => purchase.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingPurchase(null);
|
||||||
|
setFormData({
|
||||||
|
date: new Date().toLocaleDateString('fa-IR'),
|
||||||
|
supplier: '',
|
||||||
|
product: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
status: 'در انتظار'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'تایید شده':
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
case 'در انتظار':
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case 'لغو شده':
|
||||||
|
return 'bg-red-100 text-red-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">مدیریت خرید</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
ثبت خرید جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{editingPurchase ? 'ویرایش خرید' : 'ثبت خرید جدید'}
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">تاریخ</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="date"
|
||||||
|
value={formData.date}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">تامینکننده</label>
|
||||||
|
<select
|
||||||
|
name="supplier"
|
||||||
|
value={formData.supplier}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">انتخاب کنید</option>
|
||||||
|
{suppliers.map(supplier => (
|
||||||
|
<option key={supplier} value={supplier}>{supplier}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">کالا</label>
|
||||||
|
<select
|
||||||
|
name="product"
|
||||||
|
value={formData.product}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">انتخاب کنید</option>
|
||||||
|
{products.map(product => (
|
||||||
|
<option key={product} value={product}>{product}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">تعداد</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="quantity"
|
||||||
|
value={formData.quantity}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">قیمت واحد (ریال)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="unitPrice"
|
||||||
|
value={formData.unitPrice}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">وضعیت</label>
|
||||||
|
<select
|
||||||
|
name="status"
|
||||||
|
value={formData.status}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="در انتظار">در انتظار</option>
|
||||||
|
<option value="تایید شده">تایید شده</option>
|
||||||
|
<option value="لغو شده">لغو شده</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button type="submit" className="btn-success flex-1">
|
||||||
|
{editingPurchase ? 'ویرایش' : 'ثبت'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleCancel} className="btn-secondary flex-1">
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Purchases Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>تاریخ</th>
|
||||||
|
<th>تامینکننده</th>
|
||||||
|
<th>کالا</th>
|
||||||
|
<th>تعداد</th>
|
||||||
|
<th>قیمت واحد</th>
|
||||||
|
<th>مجموع</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{purchases.map(purchase => (
|
||||||
|
<tr key={purchase.id}>
|
||||||
|
<td>{purchase.date}</td>
|
||||||
|
<td>{purchase.supplier}</td>
|
||||||
|
<td>{purchase.product}</td>
|
||||||
|
<td>{purchase.quantity}</td>
|
||||||
|
<td>{formatPrice(purchase.unitPrice)}</td>
|
||||||
|
<td className="font-bold">{formatPrice(purchase.total)}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(purchase.status)}`}>
|
||||||
|
{purchase.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(purchase)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(purchase.id)}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchaseManagement;
|
||||||
344
src/components/SalesManagement.js
Normal file
344
src/components/SalesManagement.js
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const SalesManagement = () => {
|
||||||
|
const [sales, setSales] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
date: '1403/01/15',
|
||||||
|
customer: 'احمد محمدی',
|
||||||
|
product: 'لپتاپ ایسوس',
|
||||||
|
quantity: 1,
|
||||||
|
unitPrice: 16000000,
|
||||||
|
total: 16000000,
|
||||||
|
status: 'تایید شده'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
date: '1403/01/14',
|
||||||
|
customer: 'فاطمه احمدی',
|
||||||
|
product: 'موبایل سامسونگ',
|
||||||
|
quantity: 2,
|
||||||
|
unitPrice: 8500000,
|
||||||
|
total: 17000000,
|
||||||
|
status: 'در انتظار'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingSale, setEditingSale] = useState(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
date: new Date().toLocaleDateString('fa-IR'),
|
||||||
|
customer: '',
|
||||||
|
product: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
status: 'در انتظار'
|
||||||
|
});
|
||||||
|
|
||||||
|
const customers = ['احمد محمدی', 'فاطمه احمدی', 'علی رضایی'];
|
||||||
|
const products = ['لپتاپ ایسوس', 'موبایل سامسونگ', 'کتاب برنامهنویسی'];
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const total = parseFloat(formData.quantity) * parseFloat(formData.unitPrice);
|
||||||
|
|
||||||
|
if (editingSale) {
|
||||||
|
// Update existing sale
|
||||||
|
setSales(sales.map(sale =>
|
||||||
|
sale.id === editingSale.id
|
||||||
|
? {
|
||||||
|
...sale,
|
||||||
|
...formData,
|
||||||
|
quantity: parseInt(formData.quantity),
|
||||||
|
unitPrice: parseFloat(formData.unitPrice),
|
||||||
|
total: total
|
||||||
|
}
|
||||||
|
: sale
|
||||||
|
));
|
||||||
|
setEditingSale(null);
|
||||||
|
} else {
|
||||||
|
// Add new sale
|
||||||
|
const newSale = {
|
||||||
|
id: Date.now(),
|
||||||
|
...formData,
|
||||||
|
quantity: parseInt(formData.quantity),
|
||||||
|
unitPrice: parseFloat(formData.unitPrice),
|
||||||
|
total: total
|
||||||
|
};
|
||||||
|
setSales([newSale, ...sales]);
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
date: new Date().toLocaleDateString('fa-IR'),
|
||||||
|
customer: '',
|
||||||
|
product: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
status: 'در انتظار'
|
||||||
|
});
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (sale) => {
|
||||||
|
setEditingSale(sale);
|
||||||
|
setFormData({
|
||||||
|
...sale,
|
||||||
|
quantity: sale.quantity.toString(),
|
||||||
|
unitPrice: sale.unitPrice.toString()
|
||||||
|
});
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm('آیا از حذف این فروش اطمینان دارید؟')) {
|
||||||
|
setSales(sales.filter(sale => sale.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingSale(null);
|
||||||
|
setFormData({
|
||||||
|
date: new Date().toLocaleDateString('fa-IR'),
|
||||||
|
customer: '',
|
||||||
|
product: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
status: 'در انتظار'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' ریال';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'تایید شده':
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
case 'در انتظار':
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case 'لغو شده':
|
||||||
|
return 'bg-red-100 text-red-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate total sales
|
||||||
|
const totalSales = sales.reduce((sum, sale) => sum + sale.total, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="farsi-text">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">مدیریت فروش</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
ثبت فروش جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sales Summary */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-green-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
💰
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">کل فروش</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{formatPrice(totalSales)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-blue-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
📊
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">تعداد فروش</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{sales.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-purple-500 rounded-full p-3 text-white text-2xl ml-4">
|
||||||
|
✅
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">فروش تایید شده</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">
|
||||||
|
{sales.filter(sale => sale.status === 'تایید شده').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Modal */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{editingSale ? 'ویرایش فروش' : 'ثبت فروش جدید'}
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">تاریخ</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="date"
|
||||||
|
value={formData.date}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">مشتری</label>
|
||||||
|
<select
|
||||||
|
name="customer"
|
||||||
|
value={formData.customer}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">انتخاب کنید</option>
|
||||||
|
{customers.map(customer => (
|
||||||
|
<option key={customer} value={customer}>{customer}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">کالا</label>
|
||||||
|
<select
|
||||||
|
name="product"
|
||||||
|
value={formData.product}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">انتخاب کنید</option>
|
||||||
|
{products.map(product => (
|
||||||
|
<option key={product} value={product}>{product}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">تعداد</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="quantity"
|
||||||
|
value={formData.quantity}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">قیمت واحد (ریال)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="unitPrice"
|
||||||
|
value={formData.unitPrice}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">وضعیت</label>
|
||||||
|
<select
|
||||||
|
name="status"
|
||||||
|
value={formData.status}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="در انتظار">در انتظار</option>
|
||||||
|
<option value="تایید شده">تایید شده</option>
|
||||||
|
<option value="لغو شده">لغو شده</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button type="submit" className="btn-success flex-1">
|
||||||
|
{editingSale ? 'ویرایش' : 'ثبت'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleCancel} className="btn-secondary flex-1">
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sales Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>تاریخ</th>
|
||||||
|
<th>مشتری</th>
|
||||||
|
<th>کالا</th>
|
||||||
|
<th>تعداد</th>
|
||||||
|
<th>قیمت واحد</th>
|
||||||
|
<th>مجموع</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sales.map(sale => (
|
||||||
|
<tr key={sale.id}>
|
||||||
|
<td>{sale.date}</td>
|
||||||
|
<td>{sale.customer}</td>
|
||||||
|
<td>{sale.product}</td>
|
||||||
|
<td>{sale.quantity}</td>
|
||||||
|
<td>{formatPrice(sale.unitPrice)}</td>
|
||||||
|
<td className="font-bold">{formatPrice(sale.total)}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(sale.status)}`}>
|
||||||
|
{sale.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex space-x-2 space-x-reverse">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(sale)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(sale.id)}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SalesManagement;
|
||||||
70
src/data/sampleInvoice.js
Normal file
70
src/data/sampleInvoice.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Sample invoice data for testing PDF generation
|
||||||
|
export const sampleInvoice = {
|
||||||
|
id: 1,
|
||||||
|
invoiceNumber: 'INV-001',
|
||||||
|
date: '1403/01/15',
|
||||||
|
customer: 'احمد محمدی',
|
||||||
|
customerAddress: 'تهران، خیابان ولیعصر، پلاک 456',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
product: 'لپتاپ ایسوس ROG',
|
||||||
|
quantity: 1,
|
||||||
|
unitPrice: 25000000,
|
||||||
|
total: 25000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'ماوس بیسیم Logitech',
|
||||||
|
quantity: 2,
|
||||||
|
unitPrice: 800000,
|
||||||
|
total: 1600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'کیبورد مکانیکی',
|
||||||
|
quantity: 1,
|
||||||
|
unitPrice: 1200000,
|
||||||
|
total: 1200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'مانیتور 27 اینچ',
|
||||||
|
quantity: 1,
|
||||||
|
unitPrice: 8000000,
|
||||||
|
total: 8000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtotal: 35800000,
|
||||||
|
tax: 3580000,
|
||||||
|
total: 39380000,
|
||||||
|
status: 'پرداخت شده'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sampleInvoice2 = {
|
||||||
|
id: 2,
|
||||||
|
invoiceNumber: 'INV-002',
|
||||||
|
date: '1403/01/16',
|
||||||
|
customer: 'شرکت فناوری پارس',
|
||||||
|
customerAddress: 'اصفهان، خیابان چهارباغ، برج تجاری پارس',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
product: 'سرور HP ProLiant',
|
||||||
|
quantity: 2,
|
||||||
|
unitPrice: 45000000,
|
||||||
|
total: 90000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'سوئیچ شبکه 24 پورت',
|
||||||
|
quantity: 3,
|
||||||
|
unitPrice: 3500000,
|
||||||
|
total: 10500000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'کابل شبکه CAT6',
|
||||||
|
quantity: 100,
|
||||||
|
unitPrice: 50000,
|
||||||
|
total: 5000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtotal: 105500000,
|
||||||
|
tax: 10550000,
|
||||||
|
total: 116050000,
|
||||||
|
status: 'در انتظار پرداخت'
|
||||||
|
};
|
||||||
68
src/index.css
Normal file
68
src/index.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Vazir', Tahoma, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom styles for Farsi text */
|
||||||
|
.farsi-text {
|
||||||
|
font-family: 'Vazir', Tahoma, sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom button styles */
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
@apply bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form styles */
|
||||||
|
.form-input {
|
||||||
|
@apply w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card styles */
|
||||||
|
.card {
|
||||||
|
@apply bg-white shadow-md rounded-lg p-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styles */
|
||||||
|
.table {
|
||||||
|
@apply w-full border-collapse border border-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
@apply bg-gray-100 border border-gray-300 px-4 py-2 text-right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
@apply border border-gray-300 px-4 py-2 text-right;
|
||||||
|
}
|
||||||
11
src/index.js
Normal file
11
src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
17
tailwind.config.js
Normal file
17
tailwind.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
'farsi': ['Vazir', 'Tahoma', 'sans-serif'],
|
||||||
|
},
|
||||||
|
direction: {
|
||||||
|
'rtl': 'rtl',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user