const { useState, useEffect, useRef } = React; const PAPER = "#EFE9DC"; const PAPER_CARD = "#F8F4EA"; const INK = "#24304A"; const INK_SOFT = "#63697A"; const CREDIT = "#2F5D4E"; const DEBIT = "#A13D2E"; const GOLD = "#B08D3F"; const LINE = "#D8CFBC"; const FALLBACK_CATEGORIES = ["עסק פרטי", "קמפ", 'בית חב"ד']; const todayStr = () => new Date().toISOString().split("T")[0]; function formatILS(n) { const num = Number(n) || 0; return `${num.toLocaleString("he-IL", { maximumFractionDigits: 2 })} ₪`; } function formatDateDisplay(dateVal) { if (!dateVal) return ""; const d = new Date(dateVal); return d.toLocaleDateString("he-IL", { day: "2-digit", month: "2-digit", year: "numeric" }); } function safeFileName(str) { return (str || "תמונה").replace(/[^א-תa-zA-Z0-9 ]/g, "").trim() || "תמונה"; } function downloadUrl(url, filename) { const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); } function resizeImageToBlob(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const maxDim = 1200; let width = img.width; let height = img.height; if (width > maxDim || height > maxDim) { if (width > height) { height = Math.round((height * maxDim) / width); width = maxDim; } else { width = Math.round((width * maxDim) / height); height = maxDim; } } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); canvas.toBlob( (blob) => { if (blob) resolve(blob); else reject(new Error("שגיאה בעיבוד תמונה")); }, "image/jpeg", 0.8 ); }; img.onerror = () => reject(new Error("שגיאת טעינת תמונה")); img.src = e.target.result; }; reader.onerror = () => reject(new Error("שגיאת קריאת קובץ")); reader.readAsDataURL(file); }); } function emptyForm() { return { date: todayStr(), type: "expense", category: "", description: "", source: "", amount: "", needsReimbursement: false, hasWarranty: false, warrantyMonths: "", photoFile: null, photoPreviewUrl: null, hadExistingPhoto: false, removeExistingPhoto: false, }; } function GlobalStyle() { return ( ); } function Icon({ children, size, style }) { return ( {children} ); } const IconPlus = (p) => ( ); const IconList = (p) => ( ); const IconShield = (p) => ( ); const IconCamera = (p) => ( ); const IconTrash = (p) => ( ); const IconX = (p) => ( ); const IconCheck = (p) => ( ); const IconImageOff = (p) => ( ); const IconDownload = (p) => ( ); const IconWallet = (p) => ( ); const IconLogOut = (p) => ( ); function PhotoThumb({ photo, size, onDownload, placeholderIcon: PlaceholderIcon, placeholderColor }) { const dim = size === "sm" ? "w-12 h-12" : "w-14 h-14"; const iconSize = size === "sm" ? 16 : 20; if (!photo) { if (PlaceholderIcon) { return (
); } return (
); } return ( ); } function EntryCard({ entry, onEdit, onToggleReimburse, onDelete, onDownloadImage, confirming, onArmDelete, onCancelDelete }) { const isIncome = entry.type === "income"; const color = isIncome ? CREDIT : DEBIT; return (
onDownloadImage(entry)} />
{entry.needsReimbursement ? ( ) : null} {confirming ? (
) : ( )}
); } function WarrantyCard({ item, onDownloadImage }) { const expired = item.days < 0; const soon = !expired && item.days <= 30; const statusColor = expired ? DEBIT : soon ? GOLD : CREDIT; const statusText = expired ? "פג תוקף" : `נותרו ${item.days} ימים`; return (
onDownloadImage(item)} placeholderIcon={IconShield} placeholderColor={statusColor} />
{item.description}
{item.category} • נרכש {formatDateDisplay(item.date)}
בתוקף עד {formatDateDisplay(item.expiry)}
{statusText}
); } function ReimbursementCard({ entry, onToggle, onDownloadImage }) { const done = entry.reimbursementDone; const statusColor = done ? CREDIT : GOLD; return (
onDownloadImage(entry)} placeholderIcon={IconWallet} placeholderColor={statusColor} />
{entry.description}
{entry.category} • {formatDateDisplay(entry.date)}
{formatILS(entry.amount)}
); } function LoadingScreen() { return (
טוען...
); } function LoginScreen({ needsSetup, onSuccess }) { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); async function handleSubmit(e) { e.preventDefault(); setError(""); if (needsSetup && password !== confirmPassword) { setError("הסיסמאות אינן תואמות"); return; } setLoading(true); try { const url = needsSetup ? "api/setup.php" : "api/login.php"; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); const data = await res.json(); if (res.ok && data.success) { onSuccess(); } else { setError((data && data.error) || "שגיאה, נסה שוב"); } } catch (err) { setError("שגיאת תקשורת עם השרת"); } setLoading(false); } return (

פנקס

{needsSetup ? "קביעת סיסמת כניסה למערכת" : "כניסה למערכת"}

setPassword(e.target.value)} placeholder="סיסמה" autoFocus className="w-full rounded-lg px-3 py-2.5 text-sm mb-3" style={{ background: PAPER, border: `1px solid ${LINE}`, color: INK }} /> {needsSetup ? ( setConfirmPassword(e.target.value)} placeholder="אימות סיסמה" className="w-full rounded-lg px-3 py-2.5 text-sm mb-3" style={{ background: PAPER, border: `1px solid ${LINE}`, color: INK }} /> ) : null} {error ? (
{error}
) : null}
); } function App() { const [authState, setAuthState] = useState("loading"); const [entries, setEntries] = useState([]); const [categories, setCategories] = useState(FALLBACK_CATEGORIES); const [tab, setTab] = useState("add"); const [form, setForm] = useState(emptyForm()); const [editingId, setEditingId] = useState(null); const [showNewCat, setShowNewCat] = useState(false); const [newCatValue, setNewCatValue] = useState(""); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [savedFlash, setSavedFlash] = useState(false); const [formError, setFormError] = useState(""); const [submitting, setSubmitting] = useState(false); const [filters, setFilters] = useState({ type: "all", category: "all", from: "", to: "" }); const [reimbFilter, setReimbFilter] = useState("all"); const fileInputRef = useRef(null); useEffect(() => { checkSession(); }, []); async function checkSession() { try { const res = await fetch("api/session.php"); const data = await res.json(); if (data.needsSetup) { setAuthState("needs-setup"); } else if (!data.authenticated) { setAuthState("needs-login"); } else { await loadAllData(); setAuthState("ready"); } } catch (err) { setAuthState("needs-login"); } } async function loadAllData() { try { const [entriesRes, catsRes] = await Promise.all([fetch("api/entries.php"), fetch("api/categories.php")]); const entriesData = await entriesRes.json(); const catsData = await catsRes.json(); setEntries(Array.isArray(entriesData) ? entriesData : []); setCategories(catsData && catsData.length ? catsData : FALLBACK_CATEGORIES); } catch (err) { console.error("שגיאה בטעינת נתונים", err); } } async function handleLoginSuccess() { await loadAllData(); setAuthState("ready"); } async function handleLogout() { try { await fetch("api/logout.php", { method: "POST" }); } catch (err) { // ignore } setEntries([]); setAuthState("needs-login"); } function resetForm() { setForm(emptyForm()); setEditingId(null); setShowNewCat(false); setNewCatValue(""); setFormError(""); } function startEdit(entry) { setForm({ date: entry.date, type: entry.type, category: entry.category, description: entry.description, source: entry.source || "", amount: String(entry.amount), needsReimbursement: entry.needsReimbursement, hasWarranty: entry.hasWarranty, warrantyMonths: entry.warrantyMonths != null ? String(entry.warrantyMonths) : "", photoFile: null, photoPreviewUrl: entry.photo || null, hadExistingPhoto: !!entry.photo, removeExistingPhoto: false, }); setEditingId(entry.id); setTab("add"); setFormError(""); } async function handleSubmit() { if (!form.category) { setFormError("צריך לבחור קטגוריה"); return; } if (!form.description.trim()) { setFormError("צריך לתאר במה מדובר"); return; } if (!form.amount || Number(form.amount) <= 0) { setFormError("צריך להזין סכום תקין"); return; } setFormError(""); setSubmitting(true); const fd = new FormData(); fd.append("date", form.date); fd.append("type", form.type); fd.append("category", form.category); fd.append("description", form.description); fd.append("source", form.source || ""); fd.append("amount", form.amount); fd.append("needsReimbursement", form.needsReimbursement ? "true" : "false"); fd.append("hasWarranty", form.hasWarranty ? "true" : "false"); if (form.hasWarranty) { fd.append("warrantyMonths", form.warrantyMonths || "0"); } if (form.photoFile) { fd.append("photo", form.photoFile, "upload.jpg"); } if (form.removeExistingPhoto) { fd.append("removePhoto", "true"); } const url = editingId ? `api/entries.php?id=${editingId}` : "api/entries.php"; try { const res = await fetch(url, { method: "POST", body: fd }); const data = await res.json(); if (!res.ok || !data.success) { setFormError((data && data.error) || "שגיאה בשמירה"); setSubmitting(false); return; } await loadAllData(); const wasEditing = !!editingId; resetForm(); if (wasEditing) { setTab("list"); } else { setSavedFlash(true); setTimeout(() => setSavedFlash(false), 1800); } } catch (err) { setFormError("שגיאת תקשורת עם השרת"); } setSubmitting(false); } async function addCategory() { const v = newCatValue.trim(); if (!v) return; try { await fetch("api/categories.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: v }), }); const catsRes = await fetch("api/categories.php"); setCategories(await catsRes.json()); } catch (err) { console.error("שגיאה בהוספת קטגוריה", err); } setForm((f) => ({ ...f, category: v })); setNewCatValue(""); setShowNewCat(false); } async function toggleReimbursed(id) { setEntries((prev) => prev.map((e) => (e.id === id ? { ...e, reimbursementDone: !e.reimbursementDone } : e))); try { await fetch(`api/entries.php?action=toggle-reimbursement&id=${id}`, { method: "POST" }); } catch (err) { console.error("שגיאה בעדכון סטטוס החזר", err); await loadAllData(); } } async function deleteEntry(id) { setConfirmDeleteId(null); setEntries((prev) => prev.filter((e) => e.id !== id)); try { await fetch(`api/entries.php?id=${id}`, { method: "DELETE" }); } catch (err) { console.error("שגיאה במחיקה", err); await loadAllData(); } } async function handlePhotoChange(e) { const file = e.target.files && e.target.files[0]; if (!file) return; try { const blob = await resizeImageToBlob(file); const previewUrl = URL.createObjectURL(blob); setForm((f) => ({ ...f, photoFile: blob, photoPreviewUrl: previewUrl, removeExistingPhoto: false })); } catch (err) { console.error("שגיאה בטעינת תמונה", err); } e.target.value = ""; } function handleRemovePhoto() { setForm((f) => { if (f.photoFile && f.photoPreviewUrl) { URL.revokeObjectURL(f.photoPreviewUrl); } return { ...f, photoFile: null, photoPreviewUrl: null, removeExistingPhoto: f.hadExistingPhoto }; }); } function downloadEntryImage(entry) { if (!entry.photo) return; downloadUrl(entry.photo, `${safeFileName(entry.description)}-${entry.date}.jpg`); } const filteredEntries = entries .filter((e) => { if (filters.type !== "all" && e.type !== filters.type) return false; if (filters.category !== "all" && e.category !== filters.category) return false; if (filters.from && e.date < filters.from) return false; if (filters.to && e.date > filters.to) return false; return true; }) .sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : b.createdAt - a.createdAt)); const totalIncome = filteredEntries.filter((e) => e.type === "income").reduce((s, e) => s + e.amount, 0); const totalExpense = filteredEntries.filter((e) => e.type === "expense").reduce((s, e) => s + e.amount, 0); const net = totalIncome - totalExpense; function exportToExcel() { if (filteredEntries.length === 0) return; const rows = filteredEntries.map((e) => ({ "תאריך": formatDateDisplay(e.date), "סוג": e.type === "income" ? "הכנסה" : "הוצאה", "קטגוריה": e.category, "תיאור": e.description, "מקור": e.source || "", "סכום": e.amount, "דרוש החזר": e.needsReimbursement ? "כן" : "לא", "בוצע החזר": e.needsReimbursement ? (e.reimbursementDone ? "כן" : "לא") : "", "יש אחריות": e.hasWarranty ? "כן" : "לא", "תקופת אחריות בחודשים": e.hasWarranty ? e.warrantyMonths : "", })); const ws = XLSX.utils.json_to_sheet(rows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "רשומות"); XLSX.writeFile(wb, `פנקס-${todayStr()}.xlsx`); } const overallIncome = entries.filter((e) => e.type === "income").reduce((s, e) => s + e.amount, 0); const overallExpense = entries.filter((e) => e.type === "expense").reduce((s, e) => s + e.amount, 0); const overallNet = overallIncome - overallExpense; const warrantyItems = entries .filter((e) => e.hasWarranty) .map((e) => { const purchase = new Date(e.date); const expiry = new Date(purchase); expiry.setMonth(expiry.getMonth() + (e.warrantyMonths || 0)); const days = Math.ceil((expiry - new Date()) / 86400000); return { ...e, expiry, days }; }) .sort((a, b) => a.days - b.days); const allReimbursable = entries.filter((e) => e.needsReimbursement); const reimbursementItems = allReimbursable .filter((e) => { if (reimbFilter === "pending") return !e.reimbursementDone; if (reimbFilter === "done") return e.reimbursementDone; return true; }) .sort((a, b) => { if (a.reimbursementDone !== b.reimbursementDone) return a.reimbursementDone ? 1 : -1; return a.date < b.date ? -1 : a.date > b.date ? 1 : 0; }); const pendingReimbTotal = allReimbursable.filter((e) => !e.reimbursementDone).reduce((s, e) => s + e.amount, 0); const doneReimbTotal = allReimbursable.filter((e) => e.reimbursementDone).reduce((s, e) => s + e.amount, 0); const filtersActive = filters.type !== "all" || filters.category !== "all" || filters.from || filters.to; const hasAnyPhoto = entries.some((e) => e.photo); if (authState === "loading") { return ; } if (authState === "needs-setup" || authState === "needs-login") { return ; } return (

פנקס

מעקב הכנסות והוצאות

יתרה כוללת
= 0 ? CREDIT : DEBIT }}> {formatILS(overallNet)}
{tab === "add" && (
{editingId ? (
עריכת רשומה קיימת
) : null}
setForm((f) => ({ ...f, date: e.target.value }))} className="w-full rounded-lg px-3 py-2.5 text-sm" style={{ background: PAPER, border: `1px solid ${LINE}`, color: INK }} />
{!showNewCat ? (
{categories.map((c) => ( ))}
) : (
setNewCatValue(e.target.value)} placeholder="שם הקטגוריה" className="flex-1 rounded-lg px-3 py-2 text-sm" style={{ background: PAPER, border: `1px solid ${LINE}`, color: INK }} onKeyDown={(e) => { if (e.key === "Enter") addCategory(); }} />
)}