feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login

This commit is contained in:
ali_mohaghegh
2026-08-25 19:08:15 +03:30
parent 170aed66ea
commit 4e105c5cd5
103 changed files with 16352 additions and 0 deletions
@@ -0,0 +1,70 @@
import type { Milestone } from "../../lib/types";
import { TermsheetCard } from "../../components/TermsheetCard";
import { itemRowLabel, type ItemDraft } from "./Step2LineItems";
export interface TsCardDraft {
key: string;
milestones: Milestone[];
}
export interface Step3Props {
cards: TsCardDraft[];
onCardsChange: (cards: TsCardDraft[]) => void;
items: ItemDraft[];
coverage: Record<string, string[]>; // cardKey → tempIdهای تیک‌خورده
onCoverageChange: (cardKey: string, ids: string[]) => void;
}
// پوشش مؤثر هر کارت: تیک‌های صریح، یا (کارت اول بدون تیک) همه‌ی اقلام
export function effectiveCovered(cards: TsCardDraft[], coverage: Record<string, string[]>, items: ItemDraft[], cardKey: string): string[] {
const explicit = coverage[cardKey];
if (explicit && explicit.length) return explicit;
if (cards[0]?.key === cardKey) return items.map((i) => i.tempId);
return [];
}
// مرحله ۳ — شرایط پرداخت (termsheetها) با چک‌لیست اقلام
export function Step3Termsheets({ cards, onCardsChange, items, coverage, onCoverageChange }: Step3Props) {
const itemOptions = items.map((it) => ({ tempId: it.tempId, label: itemRowLabel(it) }));
const addCard = () => {
onCardsChange([
...cards,
{ key: "new-ts-" + Date.now() + Math.random().toString(36).slice(2, 6), milestones: [{ when_iso: null, amount: null }] },
]);
};
const removeCard = (key: string) => {
onCardsChange(cards.filter((c) => c.key !== key));
onCoverageChange(key, []);
};
return (
<div>
<h3 className="mb-1 text-[15px] font-bold">
<span className="me-1 inline-flex h-6 w-6 items-center justify-center rounded-full bg-accent text-[12px] font-bold text-white">۳</span>
شرایط پرداخت — termsheet ها
</h3>
<div className="mb-3 text-[12px] text-muted" dangerouslySetInnerHTML={{ __html: 'هر <b>termsheet</b> یک روش پرداخت است (نقد، چک، اقساط...). داخل هر termsheet مشخص کنید کدام قلم‌ها را پوشش می‌دهد — یک termsheet می‌تواند چند قلم را بگیرد، و یک قلم هم می‌تواند زیر چند termsheet مختلف (مثلاً هم نقد هم چک) پیشنهاد شود.' }} />
<div className="flex flex-col gap-3">
{cards.map((card, idx) => (
<TermsheetCard
key={card.key}
index={idx + 1}
milestones={card.milestones}
onMilestonesChange={(ms) => onCardsChange(cards.map((c) => (c.key === card.key ? { ...c, milestones: ms } : c)))}
items={itemOptions}
coveredIds={effectiveCovered(cards, coverage, items, card.key)}
onCoverageChange={(ids) => onCoverageChange(card.key, ids)}
onRemove={() => removeCard(card.key)}
/>
))}
</div>
<button type="button" className="btn-line-file mt-3" onClick={addCard}>
+ افزودن termsheet
</button>
</div>
);
}