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
+101
View File
@@ -0,0 +1,101 @@
import type { Milestone } from "../lib/types";
import { faNum } from "../lib/formatters";
import { MilestoneRow } from "./MilestoneRow";
export interface TermsheetItem {
tempId: string;
label: string;
}
export interface TermsheetCardProps {
index: number; // 1-based → «گزینه N»
milestones: Milestone[];
onMilestonesChange: (ms: Milestone[]) => void;
// اقلام قابل تیک (مرحله ۲)
items?: TermsheetItem[];
coveredIds?: string[]; // tempIdهایی که تیک دارند
onCoverageChange?: (ids: string[]) => void;
onRemove?: () => void;
defaultAll?: boolean; // وقتی هیچ تیکی نیست، پیش‌فرض همه تیک بخورند
removable?: boolean;
}
// کارت ترم‌شیت — هم در ویزارد (مرحله ۳) هم در مودال پذیرش/ویرایش
export function TermsheetCard({
index,
milestones,
onMilestonesChange,
items = [],
coveredIds = [],
onCoverageChange,
onRemove,
defaultAll = false,
removable = true,
}: TermsheetCardProps) {
const covered = new Set(coveredIds);
// پیش‌فرض: وقتی هیچ قلمی تیک نخورده و defaultAll فعال است → همه تیک
const effectiveCovered = covered.size === 0 && defaultAll ? items.map((i) => i.tempId) : coveredIds;
const toggle = (tempId: string, checked: boolean) => {
if (!onCoverageChange) return;
const next = new Set(covered);
if (checked) next.add(tempId);
else next.delete(tempId);
onCoverageChange([...next]);
};
const addMilestone = () => {
onMilestonesChange([...milestones, { when_iso: null, amount: null }]);
};
return (
<div className="variant-card">
<div className="variant-head">
<span className="variant-num">گزینه {faNum(index)}</span>
{removable && onRemove && (
<button type="button" className="variant-x" title="حذف" onClick={onRemove}>
×
</button>
)}
</div>
<div className="mb-1.5 text-[12px] text-muted">مراحل پرداخت:</div>
<div className="mb-1 flex flex-col gap-1.5">
{milestones.map((m, i) => (
<MilestoneRow
key={i}
milestone={m}
onChange={(next) => onMilestonesChange(milestones.map((x, j) => (j === i ? next : x)))}
onRemove={() => onMilestonesChange(milestones.filter((_, j) => j !== i))}
/>
))}
</div>
<button type="button" className="btn-line-file mt-1" onClick={addMilestone}>
+ مرحله پرداخت
</button>
{items.length > 0 && (
<div className="ts-items-box">
<div className="ts-items-title">این termsheet کدام اقلام (مرحله ۲) را پوشش می‌دهد؟</div>
{items.length === 0 ? (
<div className="ts-items-empty">هنوز قلمی در مرحله‌ی ۲ اضافه نشده.</div>
) : (
<div>
{items.map((it) => (
<label key={it.tempId} className="ts-item-label">
<input
type="checkbox"
className="ts-item-cb"
data-item-temp-id={it.tempId}
checked={effectiveCovered.includes(it.tempId)}
onChange={(e) => toggle(it.tempId, e.target.checked)}
/>
{it.label}
</label>
))}
</div>
)}
</div>
)}
</div>
);
}