'use client' import { useState, useEffect, useRef } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { useParams} from 'next/navigation' import Link from 'next/link' import { toast } from 'react-hot-toast' import { ArrowLeft, FileText, Calendar, Building2, DollarSign, History, Plus, Loader2, CheckCircle2, Upload, ExternalLink, MapPin, } from 'lucide-react' import ProtectedRoute from '@/components/ProtectedRoute' import LoadingSpinner from '@/components/LoadingSpinner' import Modal from '@/components/Modal' import { tendersAPI, Tender, TenderDirective, CreateDirectiveData } from '@/lib/api/tenders' import { contactsAPI } from '@/lib/api/contacts' import { pipelinesAPI } from '@/lib/api/pipelines' import { useLanguage } from '@/contexts/LanguageContext' const DIRECTIVE_TYPE_LABELS: Record = { BUY_TERMS: 'Buy terms booklet', VISIT_CLIENT: 'Visit client', MEET_COMMITTEE: 'Meet committee', PREPARE_TO_BID: 'Prepare to bid', } const getDisplayFileName = (attachment: any) => { const name = String(attachment.originalName || attachment.fileName || 'file') if (!/[ÃÄÅØÙ]/.test(name)) { return name } try { const bytes = new Uint8Array( Array.from(name, (char: string) => char.charCodeAt(0) & 0xff) ) return new TextDecoder('utf-8').decode(bytes) } catch { return name } } function TenderDetailContent() { const searchParams = useSearchParams() const params = useParams() const router = useRouter() const tenderId = params.id as string const { t } = useLanguage() const [tender, setTender] = useState(null) const [history, setHistory] = useState([]) const [loading, setLoading] = useState(true) type TenderTab = 'info' | 'directives' | 'attachments' | 'history' const [activeTab, setActiveTab] = useState('info') const openTab = (tab: TenderTab) => { setActiveTab(tab) router.replace(`/tenders/${params.id}?tab=${tab}`) } const [showDirectiveModal, setShowDirectiveModal] = useState(false) const [showConvertModal, setShowConvertModal] = useState(false) const [showCompleteModal, setShowCompleteModal] = useState(null) const [employees, setEmployees] = useState([]) const [contacts, setContacts] = useState([]) const [pipelines, setPipelines] = useState([]) const [directiveForm, setDirectiveForm] = useState({ type: 'BUY_TERMS', assignedToEmployeeId: '', notes: '', }) const [convertForm, setConvertForm] = useState({ contactId: '', pipelineId: '', ownerId: '', }) const [completeNotes, setCompleteNotes] = useState('') const [directiveTypeValues, setDirectiveTypeValues] = useState([]) const [submitting, setSubmitting] = useState(false) const directiveFileInputRef = useRef(null) const [uploadingDirectiveId, setUploadingDirectiveId] = useState(null) const [directiveIdForUpload, setDirectiveIdForUpload] = useState(null) const [uploadingCategory, setUploadingCategory] = useState(null) const termsInputRef = useRef(null) const costInputRef = useRef(null) const offersInputRef = useRef(null) const fetchTender = async () => { try { const data = await tendersAPI.getById(tenderId) setTender(data) } catch { toast.error(t('tenders.loadError')) } finally { setLoading(false) } } const fetchHistory = async () => { try { const data = await tendersAPI.getHistory(tenderId) setHistory(data) } catch {} } useEffect(() => { const tabParam = searchParams.get('tab') as TenderTab | null const allowedTabs: TenderTab[] = ['info', 'directives', 'attachments', 'history'] if (tabParam && allowedTabs.includes(tabParam)) { setActiveTab(tabParam) } }, [searchParams]) useEffect(() => { fetchTender() }, [tenderId]) useEffect(() => { if (tender) fetchHistory() }, [tender?.id]) useEffect(() => { tendersAPI.getDirectiveTypeValues().then(setDirectiveTypeValues).catch(() => {}) }, []) useEffect(() => { if (showDirectiveModal || showConvertModal) { // Use the directive-scoped employee list so non-HR users (with // tenders:directives:create) can populate this dropdown without // being granted hr:employees:read (which would leak salaries etc.). tendersAPI .getAssignableEmployees() .then((list) => setEmployees(list)) .catch(() => {}) } if (showConvertModal) { contactsAPI .getAll({ pageSize: 500 }) .then((r: any) => setContacts(r.contacts || [])) .catch(() => {}) pipelinesAPI.getAll().then(setPipelines).catch(() => {}) } }, [showDirectiveModal, showConvertModal]) const handleAddDirective = async (e: React.FormEvent) => { e.preventDefault() if (!directiveForm.assignedToEmployeeId) { toast.error(t('tenders.assignee') + ' ' + t('common.required')) return } setSubmitting(true) try { await tendersAPI.createDirective(tenderId, directiveForm) toast.success('Directive created') setShowDirectiveModal(false) setDirectiveForm({ type: 'BUY_TERMS', assignedToEmployeeId: '', notes: '' }) fetchTender() } catch (err: any) { toast.error(err.response?.data?.message || 'Failed') } finally { setSubmitting(false) } } const handleCompleteDirective = async (e: React.FormEvent) => { e.preventDefault() if (!showCompleteModal) return setSubmitting(true) try { await tendersAPI.updateDirective(showCompleteModal.id, { status: 'COMPLETED', completionNotes: completeNotes, }) toast.success('Task completed') setShowCompleteModal(null) setCompleteNotes('') fetchTender() } catch (err: any) { toast.error(err.response?.data?.message || 'Failed') } finally { setSubmitting(false) } } const handleConvertToDeal = async (e: React.FormEvent) => { e.preventDefault() if (!convertForm.contactId || !convertForm.pipelineId) { toast.error('Contact and Pipeline are required') return } setSubmitting(true) try { const deal = await tendersAPI.convertToDeal(tenderId, convertForm) toast.success('Tender converted to deal') setShowConvertModal(false) router.push(`/crm/deals/${deal.id}`) } catch (err: any) { toast.error(err.response?.data?.message || 'Failed') } finally { setSubmitting(false) } } const handleTenderFileUpload = async ( e: React.ChangeEvent, category?: string, ) => { const files = Array.from(e.target.files || []) if (!files.length) return if (category) setUploadingCategory(category) else setSubmitting(true) let successCount = 0 let failCount = 0 try { // Upload files sequentially so a failure of one file doesn't break the rest. for (const file of files) { try { await tendersAPI.uploadTenderAttachment(tenderId, file, category) successCount++ } catch (err: any) { failCount++ const msg = err.response?.data?.message || 'Upload failed' toast.error(`${file.name}: ${msg}`) } } if (successCount > 0) { toast.success( files.length === 1 ? t('tenders.uploadFile') : `${successCount}/${files.length} ✓` ) } if (successCount > 0) fetchTender() } finally { setSubmitting(false) setUploadingCategory(null) e.target.value = '' } } const handleDirectiveFileSelect = (directiveId: string) => { setDirectiveIdForUpload(directiveId) setTimeout(() => directiveFileInputRef.current?.click(), 0) } const handleDirectiveFileUpload = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) const directiveId = directiveIdForUpload e.target.value = '' setDirectiveIdForUpload(null) if (!files.length || !directiveId) return setUploadingDirectiveId(directiveId) let successCount = 0 try { for (const file of files) { try { await tendersAPI.uploadDirectiveAttachment(directiveId, file) successCount++ } catch (err: any) { const msg = err.response?.data?.message || 'Upload failed' toast.error(`${file.name}: ${msg}`) } } if (successCount > 0) { toast.success( files.length === 1 ? t('tenders.uploadFile') : `${successCount}/${files.length} ✓` ) fetchTender() } } finally { setUploadingDirectiveId(null) } } if (loading || !tender) { return (
) } const tabs = [ { id: 'info', label: t('tenders.titleLabel') || 'Info', icon: FileText }, { id: 'directives', label: t('tenders.directives'), icon: CheckCircle2 }, { id: 'attachments', label: t('tenders.attachments'), icon: Upload }, { id: 'history', label: t('tenders.history'), icon: History }, ] return (

{tender.tenderNumber} – {tender.title}

{tender.issuingBodyName}

{tender.status === 'ACTIVE' && ( )}
{tabs.map((tab) => ( ))}
{activeTab === 'info' && (

{t('tenders.announcementDate')}

{tender.announcementDate?.split('T')[0]}

{t('tenders.closingDate')}

{tender.closingDate?.split('T')[0]}

{t('tenders.termsValue')}

{Number(tender.termsValue)} SAR

التأمينات الأولية

{Number(tender.initialBondValue || tender.bondValue || 0)} SAR

التأمينات النهائية

{Number(tender.finalBondValue || 0)} SAR

زمن الاسترجاع

{tender.finalBondRefundPeriod || '-'}

زيارة الموقع

{tender.siteVisitRequired ? 'إجبارية' : 'غير إجبارية'}

{tender.siteVisitRequired && (

مكان الزيارة

{tender.siteVisitLocation || '-'}

)}

مكان استلام دفتر الشروط

{tender.termsPickupProvince || '-'}

{tender.announcementLink && (

{t('tenders.announcementLink')}

)} {tender.notes && (

{t('common.notes')}

{tender.notes}

)}
)} {activeTab === 'directives' && (

{t('tenders.directives')}

{!tender.directives?.length ? (

{t('common.noData')}

) : (
    {tender.directives.map((d) => (
  • {DIRECTIVE_TYPE_LABELS[d.type] || d.type}

    {d.assignedToEmployee ? `${d.assignedToEmployee.firstName} ${d.assignedToEmployee.lastName}` : ''}{' '} · {d.status}

    {d.completionNotes && (

    {d.completionNotes}

    )}
    {d.status !== 'COMPLETED' && d.assignedToEmployee?.user?.id && ( )}
  • ))}
)}
)} {activeTab === 'attachments' && (
{(() => { const all = (tender.attachments || []) as any[] const sections: Array<{ key: string label: string category: string ref: React.RefObject }> = [ { key: 'terms', label: 'دفتر الشروط', category: 'TERMS_BOOKLET', ref: termsInputRef }, { key: 'cost', label: 'cost sheet ', category: 'COST_SHEET', ref: costInputRef }, { key: 'offers', label: 'proposal', category: 'OFFERS', ref: offersInputRef }, ] // Legacy attachments without a recognized category live under // the dafter section by default so nothing gets hidden. const knownCategories = new Set(sections.map((s) => s.category)) const inSection = (a: any, category: string) => a.category === category || (category === 'TERMS_BOOKLET' && (!a.category || !knownCategories.has(a.category))) return (
{sections.map((section) => { const items = all.filter((a) => inSection(a, section.category)) const isUploading = uploadingCategory === section.category return (

{section.label}

handleTenderFileUpload(e, section.category)} />
{items.length === 0 ? (

{t('common.noData')}

) : (
    {items.map((a: any) => (
  • {getDisplayFileName(a)}
  • ))}
)}
) })}
) })()}
)} {activeTab === 'history' && (
{history.length === 0 ? (

{t('common.noData')}

) : (
    {history.map((h: any) => (
  • {h.action} · {h.user?.username} ·{' '} {h.createdAt?.split('T')[0]}
  • ))}
)}
)}
setShowDirectiveModal(false)} title={t('tenders.addDirective')} >