import { useState } from "react"; import { Link } from "react-router-dom"; import { toast } from "sonner"; import { Building2, Inbox, CheckCircle2, Truck, Phone, FolderKanban } from "lucide-react"; import type { Project, Quote, Store } from "../../lib/types"; import { nextId } from "../../lib/api"; import { useAddProject, useMaterials, useProjects, useQuotes, useStoreData, useSuppliers, useUnits, } from "../../hooks"; import { normalize } from "../../lib/normalize"; import { useAppStore } from "../../store"; import { lineItemTotal, materialById, materialLabel, supplierById, } from "../../lib/quoteUtils"; import { faNum, fmtDate } from "../../lib/formatters"; import { Header, PageLayout } from "../../components/layout/Header"; import { EmptyState } from "../../components/EmptyState"; import { Button } from "../../components/ui/button"; function projectQuotes(quotes: Quote[], name: string): Quote[] { return quotes.filter( (q) => (q.projects || []).includes(name) || q.project === name || (q.line_items || []).some( (li) => li.target_project?.name === name || (li.projects || []).some((p) => (typeof p === "string" ? p : p.name) === name), ), ); } // آیا این پیشنهاد برای «این» پروژه پذیرفته شده است؟ // ملاک، پروژه‌ی هدفِ تصمیم خریدِ اقلام است (item_decisions.project_id) — نه // وضعیت سراسری پیشنهاد؛ پیشنهادی که فقط برای پروژه‌ی دیگری پذیرفته شده، // برای این پروژه همچنان «پیشنهاد شده» است. function acceptedForProject(q: Quote, name: string): boolean { return (q.line_items || []).some((li) => !li.rejected && li.target_project?.name === name); } // آیا اصلاً برای این پیشنهاد پروژه‌ی هدفی ثبت شده؟ (پیشنهادهای قدیمیِ // پیش از era تصمیم‌پروژه، طبق وضعیت سراسری دسته‌بندی می‌شوند) function hasTargets(q: Quote): boolean { return (q.line_items || []).some((li) => !!li.target_project); } // اقلام مربوط به این پروژه در یک quote: // اولویت با اقلامِ پذیرفته‌شده برای همین پروژه (تصمیم خرید)؛ سپس کاندیداهای // همین پروژه؛ و اگر هیچ اتصال قلم‌به‌قلمی نبود، همه‌ی اقلام فعال. function itemsForProject(q: Quote, name: string): Quote["line_items"] { const items = (q.line_items || []).filter((li) => !li.rejected); const targeted = items.filter((li) => li.target_project?.name === name); if (targeted.length) return targeted; if (items.some((li) => li.projects && li.projects.length)) { return items.filter((li) => (li.projects || []).some((p) => p.name === name)); } return items; } function daysSince(iso: string | null): number | null { if (!iso) return null; const d = new Date(iso); if (Number.isNaN(d.getTime())) return null; return Math.floor((Date.now() - d.getTime()) / 86400000); } // صفحه‌ی پروژه‌ها — لیست + پنل جزئیات ۳ ستونه export function ProjectsPage() { useStoreData(); const projects = useProjects(); const quotes = useQuotes(); const suppliers = useSuppliers(); const materials = useMaterials(); const { standardConversions } = useUnits(); const addProject = useAddProject(); const topics = useAppStore((s) => s.topics); const store: Store = { quotes, suppliers, materials, projects, standardConversions, topics }; const [openProject, setOpenProject] = useState(null); const [newName, setNewName] = useState(""); const addProj = () => { const name = newName.trim(); if (!name) return; const dup = projects.some((p) => normalize(p.name) === normalize(name)); if (dup) { toast.error("این پروژه قبلاً ثبت شده است."); return; } const p: Project = { id: nextId("p", projects), name }; void addProject.mutateAsync(p); setNewName(""); setOpenProject(name); toast.success("پروژه افزوده شد."); }; return (

+ افزودن پروژه‌ی جدید

setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addProj(); } }} />
{!projects.length && !quotes.length ? ( ) : (
{projects.map((p) => { const count = projectQuotes(quotes, p.name).length; const open = openProject === p.name; return (
{/* جزئیات دقیقاً زیر همان پروژه باز می‌شود */} {open &&
{renderProjectDetail(store, p.name)}
}
); })}
)}
); } // ---------- پنل جزئیات یک پروژه ---------- function renderProjectDetail(store: Store, name: string): React.ReactElement { const quotes = projectQuotes(store.quotes, name); // جمع مبلغ هر ستون فقط روی اقلامِ همین پروژه const itemsTotal = (q: Quote) => itemsForProject(q, name).reduce((s, li) => s + (lineItemTotal(li) || 0), 0); const sumBy = (list: Quote[]) => list.reduce((s, q) => s + itemsTotal(q), 0); const matName = (mid: string) => { const m = materialById(store.materials, mid); return materialLabel(m); }; const supName = (sid: string) => { const s = supplierById(store.suppliers, sid); if (s.name === "؟") return "؟"; return ( {s.name} ); }; const itemLine = (li: Quote["line_items"][number]) => { const total = lineItemTotal(li); return (
{matName(li.material_id)} — {faNum(li.quantity)} {li.unit} × {faNum(li.unit_price || 0)} ={" "} {faNum(total || 0)} تومان
); }; // دسته‌بندی ایزوله per-project: // - پذیرفته/رسیده فقط وقتی حداقل یک قلمِ غیرردشده برای «این» پروژه پذیرفته شده باشد؛ // - پیشنهادی که صرفاً کاندیداست (یا فقط برای پروژه‌ی دیگری پذیرفته شده) → پیشنهاد شده. // - پیشنهادهای قدیمیِ بدون پروژه‌ی هدف، طبق وضعیت سراسری می‌مانند. const offered = quotes .filter( (q) => q.status === "offered" || ((q.status === "accepted" || q.status === "arrived") && hasTargets(q) && !acceptedForProject(q, name)), ) .sort((a, b) => new Date(a.expiry_date || 0).getTime() - new Date(b.expiry_date || 0).getTime()); const accepted = quotes.filter((q) => q.status === "accepted" && acceptedForProject(q, name)); const arrived = quotes.filter((q) => q.status === "arrived" && acceptedForProject(q, name)); const offeredRow = (q: Quote) => { const days = q.expiry_date ? Math.ceil((new Date(q.expiry_date).getTime() - Date.now()) / 86400000) : null; return (
{supName(q.supplier_id)}
{itemsForProject(q, name).map(itemLine)}
جمع: {faNum(itemsTotal(q))} تومان
اعتبار تا {fmtDate(q.expiry_date)}
{q.expiry_date == null ? ( بدون انقضا ) : days != null && days < 0 ? ( منقضی شده ) : days === 0 ? ( امروز آخرین مهلت ) : ( {faNum(days)} روز مانده )}
); }; const acceptedRow = (q: Quote) => { const d = daysSince(q.accepted_date); return (
{supName(q.supplier_id)}
{itemsForProject(q, name).map(itemLine)}
تایید: {fmtDate(q.accepted_date)} {d != null && {faNum(d)} روز پیش}
); }; const arrivedRow = (q: Quote) => (
{supName(q.supplier_id)}
{itemsForProject(q, name).map(itemLine)}
هزینه: {faNum(itemsTotal(q))} تومان
رسید: {fmtDate(q.arrived_date)}
); return (

{name}

کل پیشنهادها: {faNum(quotes.length)} ارزش باز: {faNum(sumBy(offered))} تومان ارزش پذیرفته: {faNum(sumBy(accepted))} تومان ارزش رسیده: {faNum(sumBy(arrived))} تومان
پیشنهاد شده ({faNum(offered.length)})
{offered.length ? offered.map(offeredRow) :
—
}
پذیرفته شده ({faNum(accepted.length)})
{accepted.length ? accepted.map(acceptedRow) :
—
}
رسیده ({faNum(arrived.length)})
{arrived.length ? arrived.map(arrivedRow) :
—
}
); }