export type PipelineAccent = 'green' | 'orange' | 'purple'; export type PipelineDeal = { id: string; title: string; description: string; amount: number | null; date: string; initials: string; /** شناسه مرحله فعلی — هنگام جابه‌جایی به‌روز می‌شود */ stageId?: string; currencyUnit?: string; }; export type PipelineUiCopy = { itemNoun: string; addItem: string; empty: string; addItemAria: string; itemMenuAria: string; listColTitle: string; hideAmount?: boolean; }; export const DEAL_BOARD_COPY: PipelineUiCopy = { itemNoun: 'معامله', addItem: 'افزودن معامله', empty: 'معامله‌ای نیست', addItemAria: 'افزودن معامله به این مرحله', itemMenuAria: 'گزینه‌های معامله', listColTitle: 'معامله', }; export const PROJECT_BOARD_COPY: PipelineUiCopy = { itemNoun: 'کارت', addItem: 'افزودن کارت', empty: 'کارتی نیست', addItemAria: 'افزودن کارت به این مرحله', itemMenuAria: 'گزینه‌های کارت', listColTitle: 'کارت', hideAmount: true, }; export type PipelineStage = { id: string; title: string; color: PipelineAccent; deals: PipelineDeal[]; isWon?: boolean; isLost?: boolean; }; export const PIPELINE_ACCENT: Record< PipelineAccent, { soft: string; solid: string; text: string; border: string } > = { green: { soft: '#ecfdf5', solid: '#10b981', text: '#047857', border: '#a7f3d0', }, orange: { soft: '#fff7ed', solid: '#f59e0b', text: '#b45309', border: '#fde68a', }, purple: { soft: '#f5f3ff', solid: '#8b5cf6', text: '#6d28d9', border: '#ddd6fe', }, }; const ACCENT_CYCLE: PipelineAccent[] = ['green', 'orange', 'purple']; /** خروجی: ۲۵۰,۰۰۰,۰۰۰ تومان */ export function formatCurrency(amount: number, unit = 'تومان'): string { try { return `${new Intl.NumberFormat('fa-IR').format(amount)} ${unit}`; } catch { return `${amount} ${unit}`; } } export function formatCurrencyParts( amount: number, unit = 'تومان', ): { number: string; unit: string } { try { return { number: new Intl.NumberFormat('fa-IR').format(amount), unit, }; } catch { return { number: String(amount), unit }; } } export function initialsFromTitle(title: string): string { const parts = title .trim() .split(/\s+/) .filter(Boolean); if (!parts.length) return '؟'; if (parts.length === 1) return parts[0]!.slice(0, 2); return parts .slice(0, 3) .map((p) => p[0]!) .join(''); } export function currencyUnitLabel(currency: string): string { if (currency === 'IRR' || currency === 'IRT') return 'تومان'; return currency; } /** نگاشت برد API به مدل UI کاریز */ export function mapBoardToStages( board: { stages: Array<{ id: string; name: string; isWon: boolean; isLost: boolean; deals: Array<{ id: string; title: string; amount: number | null; currency: string; notes: string | null; stageId: string; expectedCloseAt: string | null; createdAt: string; person: { fullName: string } | null; company: { name: string } | null; }>; }>; }, formatDate: (iso: string) => string, ): PipelineStage[] { let openIndex = 0; return board.stages.map((stage) => { let color: PipelineAccent; if (stage.isWon) color = 'green'; else if (stage.isLost) color = 'orange'; else { color = ACCENT_CYCLE[openIndex % ACCENT_CYCLE.length]!; openIndex += 1; } return { id: stage.id, title: stage.name, color, isWon: stage.isWon, isLost: stage.isLost, deals: stage.deals.map((deal) => { const description = deal.notes?.trim() || deal.company?.name || deal.person?.fullName || ''; const dateIso = deal.expectedCloseAt || deal.createdAt; return { id: deal.id, title: deal.title, description, amount: deal.amount ?? 0, date: formatDate(dateIso), initials: initialsFromTitle( deal.company?.name || deal.title, ), stageId: deal.stageId, currencyUnit: currencyUnitLabel(deal.currency), }; }), }; }); } export function mapProjectBoardToStages( board: { stages: Array<{ id: string; name: string; cards: Array<{ id: string; title: string; notes: string | null; stageId: string; createdAt: string; person: { fullName: string } | null; company: { name: string } | null; deal: { title: string } | null; }>; }>; }, formatDate: (iso: string) => string, ): PipelineStage[] { return board.stages.map((stage, index) => ({ id: stage.id, title: stage.name, color: ACCENT_CYCLE[index % ACCENT_CYCLE.length]!, deals: stage.cards.map((card) => ({ id: card.id, title: card.title, description: card.company?.name || card.person?.fullName || card.deal?.title || card.notes?.trim() || '', amount: null, date: formatDate(card.createdAt), initials: initialsFromTitle( card.company?.name || card.person?.fullName || card.title, ), stageId: card.stageId, })), })); } /** * جابه‌جایی معامله بین مراحل یا تغییر ترتیب داخل یک مرحله. * فقط state محلی را تغییر می‌دهد — آماده اتصال به API. */ export function updateDealStage( stages: PipelineStage[], dealId: string, stageId: string, toIndex?: number, ): PipelineStage[] { let moving: PipelineDeal | null = null; let fromStageId = ''; let fromIndex = -1; for (const stage of stages) { const idx = stage.deals.findIndex((d) => d.id === dealId); if (idx !== -1) { moving = stage.deals[idx]!; fromStageId = stage.id; fromIndex = idx; break; } } if (!moving) return stages; const without = stages.map((stage) => { if (stage.id !== fromStageId) return stage; return { ...stage, deals: stage.deals.filter((d) => d.id !== dealId), }; }); return without.map((stage) => { if (stage.id !== stageId) return stage; const next = [...stage.deals]; let insertAt = toIndex == null || toIndex < 0 || toIndex > next.length ? next.length : toIndex; if ( fromStageId === stageId && fromIndex >= 0 && toIndex != null && toIndex > fromIndex ) { insertAt = Math.max(0, insertAt - 1); } next.splice(insertAt, 0, { ...moving!, stageId }); return { ...stage, deals: next }; }); } export type PipelineBoardFilter = { query?: string; amountMin?: number | null; amountMax?: number | null; }; export function filterStagesByQuery( stages: PipelineStage[], query: string, ): PipelineStage[] { return filterStages(stages, { query }); } export function filterStages( stages: PipelineStage[], filter: PipelineBoardFilter, ): PipelineStage[] { const q = filter.query?.trim().toLowerCase() ?? ''; const min = filter.amountMin; const max = filter.amountMax; const hasFilter = Boolean(q) || (min != null && !Number.isNaN(min)) || (max != null && !Number.isNaN(max)); if (!hasFilter) return stages; return stages.map((stage) => ({ ...stage, deals: stage.deals.filter((d) => { if ( q && !d.title.toLowerCase().includes(q) && !d.description.toLowerCase().includes(q) ) { return false; } if (min != null && !Number.isNaN(min) && (d.amount == null || d.amount < min)) { return false; } if (max != null && !Number.isNaN(max) && (d.amount == null || d.amount > max)) { return false; } return true; }), })); } export function toFaItemCount(n: number, noun = 'معامله'): string { return `${new Intl.NumberFormat('fa-IR').format(n)} ${noun}`; } export function toFaDealCount(n: number): string { return toFaItemCount(n, 'معامله'); } /** جابه‌جایی یک ستون به ایندکس جدید */ export function moveStageToIndex( stages: PipelineStage[], stageId: string, toIndex: number, ): PipelineStage[] { const from = stages.findIndex((s) => s.id === stageId); if (from < 0) return stages; const next = [...stages]; const [item] = next.splice(from, 1); if (!item) return stages; const clamped = Math.max(0, Math.min(toIndex, next.length)); next.splice(clamped, 0, item); return next; }