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