edit for portal & tender
This commit is contained in:
@@ -87,10 +87,13 @@ function TenderDetailContent() {
|
||||
const [completeNotes, setCompleteNotes] = useState('')
|
||||
const [directiveTypeValues, setDirectiveTypeValues] = useState<string[]>([])
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const directiveFileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [uploadingDirectiveId, setUploadingDirectiveId] = useState<string | null>(null)
|
||||
const [directiveIdForUpload, setDirectiveIdForUpload] = useState<string | null>(null)
|
||||
const [uploadingCategory, setUploadingCategory] = useState<string | null>(null)
|
||||
const termsInputRef = useRef<HTMLInputElement>(null)
|
||||
const costInputRef = useRef<HTMLInputElement>(null)
|
||||
const offersInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const fetchTender = async () => {
|
||||
try {
|
||||
@@ -213,11 +216,15 @@ function TenderDetailContent() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTenderFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleTenderFileUpload = async (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
category?: string,
|
||||
) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (!files.length) return
|
||||
|
||||
setSubmitting(true)
|
||||
if (category) setUploadingCategory(category)
|
||||
else setSubmitting(true)
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
@@ -225,7 +232,7 @@ function TenderDetailContent() {
|
||||
// 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)
|
||||
await tendersAPI.uploadTenderAttachment(tenderId, file, category)
|
||||
successCount++
|
||||
} catch (err: any) {
|
||||
failCount++
|
||||
@@ -244,6 +251,7 @@ function TenderDetailContent() {
|
||||
if (successCount > 0) fetchTender()
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
setUploadingCategory(null)
|
||||
e.target.value = ''
|
||||
}
|
||||
}
|
||||
@@ -525,66 +533,102 @@ function TenderDetailContent() {
|
||||
|
||||
{activeTab === 'attachments' && (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleTenderFileUpload}
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={submitting}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
|
||||
>
|
||||
{submitting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
{t('tenders.uploadFile')}
|
||||
</button>
|
||||
</div>
|
||||
{(() => {
|
||||
const all = (tender.attachments || []) as any[]
|
||||
const sections: Array<{
|
||||
key: string
|
||||
label: string
|
||||
category: string
|
||||
ref: React.RefObject<HTMLInputElement>
|
||||
}> = [
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
{!tender.attachments?.length ? (
|
||||
<p className="text-gray-500">{t('common.noData')}</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{tender.attachments.map((a: any) => (
|
||||
<li
|
||||
key={a.id}
|
||||
className="flex items-center justify-between border rounded px-3 py-2"
|
||||
>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_API_URL}/tenders/attachments/${a.id}/view`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-indigo-600 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
{getDisplayFileName(a)}
|
||||
</a>
|
||||
// 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)))
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm('حذف الملف؟')) return
|
||||
try {
|
||||
await tendersAPI.deleteAttachment(a.id)
|
||||
toast.success('تم الحذف')
|
||||
fetchTender()
|
||||
} catch {
|
||||
toast.error('فشل الحذف')
|
||||
}
|
||||
}}
|
||||
className="text-red-600 text-sm hover:underline"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{sections.map((section) => {
|
||||
const items = all.filter((a) => inSection(a, section.category))
|
||||
const isUploading = uploadingCategory === section.category
|
||||
return (
|
||||
<div key={section.key} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-base font-semibold text-gray-900">{section.label}</h3>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
ref={section.ref}
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => handleTenderFileUpload(e, section.category)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => section.ref.current?.click()}
|
||||
disabled={isUploading}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 text-sm"
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
{t('tenders.uploadFile')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">{t('common.noData')}</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{items.map((a: any) => (
|
||||
<li
|
||||
key={a.id}
|
||||
className="flex items-center justify-between border rounded px-3 py-2"
|
||||
>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_API_URL}/tenders/attachments/${a.id}/view`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-indigo-600 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
{getDisplayFileName(a)}
|
||||
</a>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm('حذف الملف؟')) return
|
||||
try {
|
||||
await tendersAPI.deleteAttachment(a.id)
|
||||
toast.success('تم الحذف')
|
||||
fetchTender()
|
||||
} catch {
|
||||
toast.error('فشل الحذف')
|
||||
}
|
||||
}}
|
||||
className="text-red-600 text-sm hover:underline"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user