Files
shokuninmarche/client/src/pages/BudgetPage.jsx
Fullstack Developer e003c7146d Initial fullstack scaffold: Events, Guests, Budget, Bookings
- Express backend with PostgreSQL (JWT auth, full CRUD)
- React + Vite + TailwindCSS frontend in Hebrew (RTL)
- Features: Digital Booking System, Guest Management, Smart Budget Management
- Docker Compose with postgres healthcheck
- Auto-runs migrations on startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 18:28:03 +00:00

196 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { useParams, Link } from 'react-router-dom'
import { Plus, Trash2, Edit, ArrowRight, TrendingUp, TrendingDown, AlertTriangle } from 'lucide-react'
import api from '../api'
const CATEGORIES = ['אולם', 'קייטרינג', 'מוזיקה/DJ', 'צילום/וידאו', 'פרחים', 'הזמנות', 'הסעות', 'אחר']
const STATUS_OPTIONS = { planned: 'מתוכנן', paid: 'שולם', partial: 'חלקי', cancelled: 'בוטל' }
export default function BudgetPage() {
const { id } = useParams()
const [items, setItems] = useState([])
const [summary, setSummary] = useState(null)
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [editItem, setEditItem] = useState(null)
const [form, setForm] = useState({ category: 'אחר', description: '', estimated_cost: '', actual_cost: '', status: 'planned' })
useEffect(() => { loadData() }, [id])
async function loadData() {
try {
const [itemsRes, summaryRes] = await Promise.all([
api.get(`/budget/event/${id}`),
api.get(`/budget/event/${id}/summary`),
])
setItems(itemsRes.data)
setSummary(summaryRes.data)
} catch (err) { console.error(err) }
finally { setLoading(false) }
}
function openCreate() {
setEditItem(null)
setForm({ category: 'אחר', description: '', estimated_cost: '', actual_cost: '', status: 'planned' })
setShowForm(true)
}
function openEdit(item) {
setEditItem(item)
setForm({ category: item.category, description: item.description || '', estimated_cost: item.estimated_cost || '', actual_cost: item.actual_cost || '', status: item.status })
setShowForm(true)
}
async function saveItem(e) {
e.preventDefault()
try {
const data = { ...form, estimated_cost: parseFloat(form.estimated_cost) || 0, actual_cost: form.actual_cost ? parseFloat(form.actual_cost) : null }
if (editItem) {
await api.put(`/budget/${editItem.id}`, data)
} else {
await api.post('/budget', { ...data, event_id: id })
}
setShowForm(false)
loadData()
} catch (err) { console.error(err) }
}
async function deleteItem(itemId) {
if (!confirm('מחוק פריט?')) return
try {
await api.delete(`/budget/${itemId}`)
loadData()
} catch (err) { console.error(err) }
}
if (loading) return <div className="flex justify-center py-20 text-gray-400">טוען...</div>
const overBudget = summary?.over_budget
return (
<div>
<div className="flex items-center gap-2 mb-6 text-sm text-gray-500">
<Link to="/" className="hover:text-indigo-600 flex items-center gap-1"><ArrowRight className="w-4 h-4" /> הדשבורד</Link>
<span>/</span>
<Link to={`/events/${id}`} className="hover:text-indigo-600">האירוע</Link>
<span>/</span>
<span className="text-gray-800 font-medium">תקציב</span>
</div>
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold text-gray-900">ניהול תקציב</h1>
<button onClick={openCreate} className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition-colors">
<Plus className="w-4 h-4" /> הוסף הוצאה
</button>
</div>
{/* Summary */}
{summary && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-gray-200 p-4">
<p className="text-xs text-gray-500 mb-1">תקציב כולל</p>
<p className="text-xl font-bold text-gray-800">{Number(summary.event_budget).toLocaleString('he-IL')}</p>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-4">
<p className="text-xs text-gray-500 mb-1">הוצאות מתוכננות</p>
<p className="text-xl font-bold text-blue-600">{Number(summary.total_estimated).toLocaleString('he-IL')}</p>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-4">
<p className="text-xs text-gray-500 mb-1">הוצאות בפועל</p>
<p className="text-xl font-bold text-green-600">{Number(summary.total_actual).toLocaleString('he-IL')}</p>
</div>
<div className={`rounded-xl border p-4 ${overBudget ? 'bg-red-50 border-red-200' : 'bg-green-50 border-green-200'}`}>
<p className="text-xs text-gray-500 mb-1">יתרת תקציב</p>
<div className="flex items-center gap-1">
{overBudget ? <TrendingDown className="w-4 h-4 text-red-500" /> : <TrendingUp className="w-4 h-4 text-green-500" />}
<p className={`text-xl font-bold ${overBudget ? 'text-red-600' : 'text-green-600'}`}>
{Number(Math.abs(summary.remaining_budget)).toLocaleString('he-IL')}
</p>
</div>
{overBudget && <p className="text-xs text-red-500 mt-1 flex items-center gap-1"><AlertTriangle className="w-3 h-3" /> חריגה מהתקציב!</p>}
</div>
</div>
)}
{/* Form */}
{showForm && (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">{editItem ? 'עריכת הוצאה' : 'הוספת הוצאה'}</h2>
<form onSubmit={saveItem} className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">קטגוריה *</label>
<select required value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">תיאור</label>
<input value={form.description} onChange={e => setForm({ ...form, description: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">עלות מוערכת () *</label>
<input type="number" min="0" required value={form.estimated_cost} onChange={e => setForm({ ...form, estimated_cost: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">עלות בפועל ()</label>
<input type="number" min="0" value={form.actual_cost} onChange={e => setForm({ ...form, actual_cost: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">סטטוס</label>
<select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
{Object.entries(STATUS_OPTIONS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
</div>
<div className="sm:col-span-2 flex gap-3">
<button type="submit" className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2 rounded-lg transition-colors">שמור</button>
<button type="button" onClick={() => setShowForm(false)} className="border border-gray-300 hover:bg-gray-50 px-6 py-2 rounded-lg transition-colors">ביטול</button>
</div>
</form>
</div>
)}
{/* Items table */}
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="text-right px-4 py-3 font-medium text-gray-600">קטגוריה</th>
<th className="text-right px-4 py-3 font-medium text-gray-600 hidden sm:table-cell">תיאור</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">מוערך</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">בפועל</th>
<th className="text-right px-4 py-3 font-medium text-gray-600 hidden md:table-cell">סטטוס</th>
<th className="px-4 py-3"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{items.length === 0 ? (
<tr><td colSpan={6} className="text-center py-8 text-gray-400">אין פריטים בתקציב</td></tr>
) : items.map(item => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-4 py-3 font-medium">{item.category}</td>
<td className="px-4 py-3 text-gray-500 hidden sm:table-cell">{item.description || '—'}</td>
<td className="px-4 py-3 text-blue-600 font-medium">{Number(item.estimated_cost).toLocaleString('he-IL')}</td>
<td className="px-4 py-3 text-green-600 font-medium">{item.actual_cost ? `${Number(item.actual_cost).toLocaleString('he-IL')}` : '—'}</td>
<td className="px-4 py-3 hidden md:table-cell">
<span className="text-xs px-2 py-1 bg-gray-100 text-gray-600 rounded-full">{STATUS_OPTIONS[item.status] || item.status}</span>
</td>
<td className="px-4 py-3">
<div className="flex gap-1 justify-end">
<button onClick={() => openEdit(item)} className="p-1.5 hover:bg-indigo-50 rounded text-indigo-600"><Edit className="w-4 h-4" /></button>
<button onClick={() => deleteItem(item.id)} className="p-1.5 hover:bg-red-50 rounded text-red-500"><Trash2 className="w-4 h-4" /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}