"use client"; import { useState } from "react"; import Link from "next/link"; import { AlertTriangle, Check, CheckCircle2, CircleDot, ExternalLink, MessageSquare, Minus, Plus, RotateCcw, Save, SlidersHorizontal, X } from "lucide-react"; import { RequestAvatar } from "@/components/requests/RequestAvatar"; import { CommentComposer, CommentDeleteButton } from "@/components/shared/CommentControls"; import { cleanLinkRows, linkHref, toLinkRows } from "@/lib/procurementLinks"; import { formatDate, formatMoney, userProfileLink } from "@/lib/shared"; import type { ProcurementItem, ProcurementItemComment, ProcurementItemExecutionStatus, ProcurementRequest, User } from "@/types/api"; interface ProcurementRequestDetailContentProps { currentUserId?: number | null; request: ProcurementRequest; displayUserName: ( person?: User | number | null, fallbackName?: string | null, fallbackEmail?: string | null, ) => string; canProcessItems?: boolean; busyKey?: string | null; canDeleteAnyComment?: boolean; onUpdateItem?: (requestId: number, itemId: number, patch: Record) => void | Promise; onReportItemIssue?: (requestId: number, itemId: number, text?: string) => void | Promise; onMarkAllReceived?: (requestId: number) => void | Promise; itemCommentsMap?: Record; itemCommentDrafts?: Record; expandedItemComments?: Record; onToggleItemComments?: (itemId: number) => void | Promise; onItemCommentDraftChange?: (itemId: number, value: string) => void; onAddItemComment?: (requestId: number, itemId: number) => void | Promise; onDeleteItemComment?: (requestId: number, itemId: number, commentId: number) => void | Promise; footer?: React.ReactNode; } type ItemProcessingDraft = { execution_status: ProcurementItemExecutionStatus; expected_delivery_date: string; actual_unit_price: string; links: string[]; ordered_quantity: string; received_quantity: string; }; const executionStatusOptions: { value: ProcurementItemExecutionStatus; label: string }[] = [ { value: "pending", label: "Не выполнено" }, { value: "ordered", label: "Заказано" }, { value: "rejected", label: "Отказано" }, { value: "received", label: "Получено" }, { value: "completed_with_issue", label: "Выполнено с замечанием" }, { value: "edited", label: "Отредактировано" }, { value: "defective", label: "Брак / перезаказ" }, ]; const problemExecutionStatuses = new Set([ "completed_with_issue", "edited", "defective", ]); const executionStatusBadgeClass = (status?: ProcurementItemExecutionStatus) => { if (status === "ordered" || status === "received") return "app-feedback-success"; if (status === "rejected") return "app-feedback-danger"; if (status && problemExecutionStatuses.has(status)) return "app-feedback-warning"; return "app-badge"; }; const toNumber = (value?: string | number | null): number => { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; }; const toOptionalInteger = (value: string): number | null => { if (value.trim() === "") return null; const parsed = Number(value); return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : null; }; const approvalIconByStatus = (status?: string) => { const normalized = String(status || "").toLowerCase(); if (normalized === "approved") { return ; } if (normalized === "rejected") { return ; } return ; }; const initialsFromName = (name: string) => name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase() || "") .join("") || "?"; const normalizeItemDraft = (item: ProcurementItem): ItemProcessingDraft => ({ execution_status: item.execution_status || "pending", expected_delivery_date: item.expected_delivery_date || "", actual_unit_price: item.actual_unit_price ? String(item.actual_unit_price) : "", links: toLinkRows(item.links), ordered_quantity: item.ordered_quantity === null || item.ordered_quantity === undefined ? "" : String(item.ordered_quantity), received_quantity: item.received_quantity === null || item.received_quantity === undefined ? "" : String(item.received_quantity), }); interface ProcurementItemCardProps { item: ProcurementItem; requestId: number; displayUserName: ( person?: User | number | null, fallbackName?: string | null, fallbackEmail?: string | null, ) => string; canEditItemProcessing: boolean; canReportIssue: boolean; currentUserId?: number | null; canDeleteAnyComment?: boolean; busyKey?: string | null; onUpdateItem?: (requestId: number, itemId: number, patch: Record) => void | Promise; onReportItemIssue?: (requestId: number, itemId: number, text?: string) => void | Promise; comments?: ProcurementItemComment[]; commentDraft?: string; commentsOpen?: boolean; onToggleComments?: (itemId: number) => void | Promise; onCommentDraftChange?: (itemId: number, value: string) => void; onAddComment?: (requestId: number, itemId: number) => void | Promise; onDeleteComment?: (requestId: number, itemId: number, commentId: number) => void | Promise; } function ProcurementItemCard({ item, requestId, displayUserName, canEditItemProcessing, canReportIssue, currentUserId, canDeleteAnyComment = false, busyKey, onUpdateItem, onReportItemIssue, comments = [], commentDraft = "", commentsOpen = false, onToggleComments, onCommentDraftChange, onAddComment, onDeleteComment, }: ProcurementItemCardProps) { const [draft, setDraft] = useState(() => normalizeItemDraft(item)); const [processingOpen, setProcessingOpen] = useState(false); const links = Array.isArray(item.links) ? item.links.filter(Boolean) : []; const commentsTotal = item.comments_count ?? comments.length; const requestedQuantity = Math.max(0, Math.trunc(toNumber(item.quantity))); const orderedQuantity = item.ordered_quantity ?? 0; const receivedQuantity = item.received_quantity ?? 0; const status = item.execution_status || "pending"; const statusLabel = item.execution_status_display || executionStatusOptions.find((option) => option.value === status)?.label || "Не выполнено"; const canCancelReceived = canEditItemProcessing && status === "received"; const canMarkIssue = Boolean( canReportIssue && onReportItemIssue && status !== "rejected" && status !== "defective" && (canEditItemProcessing || status === "received") ); const updateDraft = (patch: Partial) => { setDraft((previous) => ({ ...previous, ...patch })); }; const adjustDraftQuantity = (field: "ordered_quantity" | "received_quantity", delta: number) => { setDraft((previous) => { const current = toOptionalInteger(previous[field]) ?? 0; const next = Math.min(requestedQuantity, Math.max(0, current + delta)); return { ...previous, [field]: String(next) }; }); }; const saveItemProcessing = () => { return onUpdateItem?.(requestId, item.id, { execution_status: draft.execution_status, expected_delivery_date: draft.expected_delivery_date || null, actual_unit_price: draft.actual_unit_price || null, ordered_quantity: toOptionalInteger(draft.ordered_quantity), received_quantity: toOptionalInteger(draft.received_quantity), links: cleanLinkRows(draft.links), }); }; const cancelReceived = () => { const nextStatus: ProcurementItemExecutionStatus = Number(item.ordered_quantity || 0) > 0 ? "ordered" : "pending"; return onUpdateItem?.(requestId, item.id, { execution_status: nextStatus, received_quantity: 0, }); }; return (

{item.name}

{item.description ? (

{item.description}

) : null} {item.supplier_info ? (

Поставщик: {item.supplier_info}

) : null}
{statusLabel}

Количество

{item.quantity} {item.unit}

Цена ориентир

{formatMoney(item.estimated_unit_price)}

Цена факт

{item.actual_unit_price ? formatMoney(item.actual_unit_price) : "—"}

Сумма расчёт

{formatMoney(item.total_price)}

Дата поступления

{formatDate(item.expected_delivery_date) || "—"}

Заказано

{orderedQuantity}/{requestedQuantity}

Получено

{receivedQuantity}/{requestedQuantity}

{links.length > 0 ? (

Ссылки / варианты

{links.map((link, linkIndex) => ( {link} ))}
) : null}
{canEditItemProcessing ? ( ) : null} {canCancelReceived ? ( ) : null} {canMarkIssue ? ( ) : null}
{commentsOpen ? (
{comments.length === 0 ? (

Комментариев по позиции пока нет

) : ( comments.map((comment) => { const canDeleteComment = Boolean( canDeleteAnyComment || (comment.author?.id && currentUserId === comment.author.id), ); return (
{displayUserName(comment.author)}
{formatDate(comment.created_at)} {canDeleteComment ? ( onDeleteComment?.(requestId, item.id, comment.id)} /> ) : null}

{comment.text}

); }) )} onCommentDraftChange?.(item.id, value)} onSubmit={() => onAddComment?.(requestId, item.id)} disabled={busyKey === `item-comment-${item.id}`} placeholder="Комментарий по позиции" />
) : null} {canEditItemProcessing && processingOpen ? (
updateDraft({ expected_delivery_date: event.target.value })} className="app-input w-full rounded-lg px-3 py-2 text-xs" />
updateDraft({ actual_unit_price: event.target.value })} className="app-input w-full rounded-lg px-3 py-2 text-xs" />
{ const parsed = toOptionalInteger(event.target.value); updateDraft({ ordered_quantity: parsed === null ? "" : String(Math.min(requestedQuantity, parsed)), }); }} placeholder={`0-${requestedQuantity}`} className="app-input min-w-0 flex-1 rounded-lg px-3 py-2 text-xs" />
{ const parsed = toOptionalInteger(event.target.value); updateDraft({ received_quantity: parsed === null ? "" : String(Math.min(requestedQuantity, parsed)), }); }} placeholder={`0-${requestedQuantity}`} className="app-input min-w-0 flex-1 rounded-lg px-3 py-2 text-xs" />
{draft.links.map((link, linkIndex) => (
updateDraft({ links: draft.links.map((currentLink, currentIndex) => ( currentIndex === linkIndex ? event.target.value : currentLink )), })} placeholder="https://example.ru/item" className="app-input min-w-0 flex-1 rounded-lg px-3 py-2 text-xs" /> {draft.links.length > 1 ? ( ) : null}
))}
) : null}
); } export function ProcurementRequestDetailContent({ currentUserId, request, displayUserName, canProcessItems = false, busyKey, canDeleteAnyComment = false, onUpdateItem, onReportItemIssue, onMarkAllReceived, itemCommentsMap = {}, itemCommentDrafts = {}, expandedItemComments = {}, onToggleItemComments, onItemCommentDraftChange, onAddItemComment, onDeleteItemComment, footer, }: ProcurementRequestDetailContentProps) { const canEditItemProcessing = Boolean(canProcessItems && onUpdateItem); const isRequestPending = String(request.status || "").toLowerCase() === "pending"; const requestorId = typeof request.requestor === "number" ? request.requestor : request.requestor?.id ?? null; const executorId = typeof request.executor === "number" ? request.executor : request.executor?.id ?? null; const canReportIssues = Boolean( !isRequestPending && onReportItemIssue && currentUserId && ( canEditItemProcessing || currentUserId === requestorId || currentUserId === executorId ), ); return (

Описание

{request.description || "—"}

Отправлена

{formatDate(request.submitted_at) || "—"}

Взята в работу

{formatDate(request.started_at) || "—"}

Завершена

{formatDate(request.completed_at) || "—"}

{request.actual_cost ? (
Фактическая сумма: {formatMoney(request.actual_cost)}
) : null}
{request.items && request.items.length > 0 ? (

Позиции

{canEditItemProcessing && onMarkAllReceived ? ( ) : null}
{request.items.map((item) => ( ))}
) : null} {request.approvals && request.approvals.length > 0 ? (

Согласования

{request.approvals.map((approval) => { const approver = typeof approval.approver === "object" && approval.approver ? approval.approver : null; const approverName = displayUserName(approval.approver, approval.approver_name); const approverLink = approver ? userProfileLink(approver, currentUserId) : ""; return (
{approvalIconByStatus(approval.status)} {approverLink ? ( {approverName} ) : ( {approverName} )} ({approval.step_label || `Этап ${approval.priority}`})
{approval.comment ? (

«{approval.comment}»

) : null}
); })}
) : null} {footer}
); }