edits for trenders attachments & claims
This commit is contained in:
@@ -38,6 +38,7 @@ const ACTIONS = [
|
||||
{ id: 'delete', name: 'حذف' },
|
||||
{ id: 'export', name: 'تصدير' },
|
||||
{ id: 'approve', name: 'اعتماد' },
|
||||
{ id: 'mark-as-paid', name: 'تأكيد القبض' },
|
||||
{ id: 'notify', name: 'إشعار' },
|
||||
{ id: 'merge', name: 'دمج' },
|
||||
];
|
||||
|
||||
@@ -39,6 +39,7 @@ const ACTIONS = [
|
||||
{ id: 'delete', name: 'حذف' },
|
||||
{ id: 'export', name: 'تصدير' },
|
||||
{ id: 'approve', name: 'اعتماد' },
|
||||
{ id: 'mark-as-paid', name: 'تأكيد القبض' },
|
||||
{ id: 'notify', name: 'إشعار' },
|
||||
{ id: 'merge', name: 'دمج' },
|
||||
];
|
||||
|
||||
@@ -17,7 +17,7 @@ type ExpenseClaimLine = {
|
||||
type ExpenseClaimFormState = {
|
||||
items: ExpenseClaimLine[];
|
||||
description: string;
|
||||
attachment: File | null;
|
||||
attachments: File[];
|
||||
};
|
||||
|
||||
const emptyLine = (): ExpenseClaimLine => ({
|
||||
@@ -32,7 +32,7 @@ const emptyLine = (): ExpenseClaimLine => ({
|
||||
const initialForm: ExpenseClaimFormState = {
|
||||
items: [emptyLine()],
|
||||
description: '',
|
||||
attachment: null,
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
function getStatusLabel(status: string) {
|
||||
@@ -178,7 +178,7 @@ export default function PortalExpenseClaimsPage() {
|
||||
await portalAPI.submitExpenseClaim({
|
||||
items,
|
||||
description: form.description.trim() || undefined,
|
||||
attachment: form.attachment,
|
||||
attachments: form.attachments,
|
||||
|
||||
});
|
||||
|
||||
@@ -286,6 +286,27 @@ export default function PortalExpenseClaimsPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{claim.status === 'APPROVED' ? (
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-lg border px-3 py-2 text-sm ${
|
||||
claim.isPaid
|
||||
? 'border-emerald-200 bg-emerald-50 text-emerald-800'
|
||||
: 'border-gray-200 bg-gray-50 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(claim.isPaid)}
|
||||
disabled
|
||||
readOnly
|
||||
className="h-4 w-4 cursor-not-allowed accent-emerald-600"
|
||||
/>
|
||||
<span className="font-medium">
|
||||
{claim.isPaid ? 'مقبوض - تم الدفع' : 'غير مقبوض'}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-2 text-sm text-gray-600">
|
||||
<div>
|
||||
<span className="font-medium text-gray-800">
|
||||
@@ -563,31 +584,58 @@ export default function PortalExpenseClaimsPage() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
مرفق
|
||||
المرفقات
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*,application/pdf"
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
attachment: e.target.files?.[0] || null,
|
||||
}))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const picked = Array.from(e.target.files || []);
|
||||
if (picked.length === 0) return;
|
||||
|
||||
setForm((prev) => {
|
||||
const combined = [...prev.attachments, ...picked];
|
||||
if (combined.length > 10) {
|
||||
alert('يمكن إرفاق 10 ملفات كحد أقصى');
|
||||
return { ...prev, attachments: combined.slice(0, 10) };
|
||||
}
|
||||
return { ...prev, attachments: combined };
|
||||
});
|
||||
|
||||
// Reset the input so picking the same file again still fires onChange.
|
||||
e.target.value = '';
|
||||
}}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
|
||||
{form.attachment ? (
|
||||
<div className="mt-2 flex items-center justify-between rounded-lg bg-gray-50 border px-3 py-2 text-sm text-gray-700">
|
||||
<span>{form.attachment.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setForm((prev) => ({ ...prev, attachment: null }))}
|
||||
className="text-red-600 hover:underline"
|
||||
>
|
||||
إزالة
|
||||
</button>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
يمكن اختيار أكثر من ملف (حتى 10 ملفات).
|
||||
</p>
|
||||
|
||||
{form.attachments.length > 0 ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
{form.attachments.map((file, idx) => (
|
||||
<div
|
||||
key={`${file.name}-${idx}`}
|
||||
className="flex items-center justify-between rounded-lg bg-gray-50 border px-3 py-2 text-sm text-gray-700"
|
||||
>
|
||||
<span className="truncate">{file.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
attachments: prev.attachments.filter((_, i) => i !== idx),
|
||||
}))
|
||||
}
|
||||
className="text-red-600 hover:underline shrink-0 ms-2"
|
||||
>
|
||||
إزالة
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { portalAPI, type ExpenseClaim } from '@/lib/api/portal';
|
||||
import Modal from '@/components/Modal';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
function formatDate(value?: string | null) {
|
||||
if (!value) return '-';
|
||||
@@ -39,10 +40,15 @@ function getStatusClasses(status: string) {
|
||||
}
|
||||
|
||||
export default function ManagedExpenseClaimsPage() {
|
||||
const { hasPermission } = useAuth();
|
||||
const canMarkAsPaid = hasPermission('department_expense_claims', 'mark-as-paid');
|
||||
|
||||
const [claims, setClaims] = useState<ExpenseClaim[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [statusFilter, setStatusFilter] = useState('PENDING');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [submittingId, setSubmittingId] = useState<string | null>(null);
|
||||
const [payingId, setPayingId] = useState<string | null>(null);
|
||||
|
||||
const [rejectModalOpen, setRejectModalOpen] = useState(false);
|
||||
const [selectedClaim, setSelectedClaim] = useState<ExpenseClaim | null>(null);
|
||||
@@ -51,11 +57,12 @@ export default function ManagedExpenseClaimsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const claimId = searchParams.get('claimId');
|
||||
|
||||
async function loadClaims(status = statusFilter) {
|
||||
async function loadClaims(status = statusFilter, search = searchQuery) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await portalAPI.getManagedExpenseClaims(
|
||||
status === 'all' ? undefined : status
|
||||
status === 'all' ? undefined : status,
|
||||
search.trim() || undefined,
|
||||
);
|
||||
setClaims(data);
|
||||
} catch (error: any) {
|
||||
@@ -66,8 +73,13 @@ export default function ManagedExpenseClaimsPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadClaims(statusFilter);
|
||||
}, [statusFilter]);
|
||||
// Debounce the search so we don't fire a request on every keystroke.
|
||||
const handle = setTimeout(() => {
|
||||
loadClaims(statusFilter, searchQuery);
|
||||
}, 400);
|
||||
return () => clearTimeout(handle);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [statusFilter, searchQuery]);
|
||||
|
||||
async function openAttachment(attachment: any) {
|
||||
try {
|
||||
@@ -96,7 +108,7 @@ export default function ManagedExpenseClaimsPage() {
|
||||
try {
|
||||
setSubmittingId(id);
|
||||
await portalAPI.approveManagedExpenseClaim(id, note.trim() || undefined);
|
||||
await loadClaims(statusFilter);
|
||||
await loadClaims(statusFilter, searchQuery);
|
||||
} catch (error: any) {
|
||||
alert(error?.response?.data?.message || 'تعذر تنفيذ الموافقة');
|
||||
} finally {
|
||||
@@ -129,7 +141,7 @@ export default function ManagedExpenseClaimsPage() {
|
||||
setRejectModalOpen(false);
|
||||
setSelectedClaim(null);
|
||||
setRejectReason('');
|
||||
await loadClaims(statusFilter);
|
||||
await loadClaims(statusFilter, searchQuery);
|
||||
} catch (error: any) {
|
||||
alert(error?.response?.data?.message || 'تعذر تنفيذ الرفض');
|
||||
} finally {
|
||||
@@ -137,6 +149,37 @@ export default function ManagedExpenseClaimsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTogglePaid(claim: ExpenseClaim, nextValue: boolean) {
|
||||
if (!canMarkAsPaid) return;
|
||||
if (payingId) return;
|
||||
|
||||
const previousValue = Boolean(claim.isPaid);
|
||||
|
||||
// Optimistic update for snappy UX.
|
||||
setClaims((prev) =>
|
||||
prev.map((c) => (c.id === claim.id ? { ...c, isPaid: nextValue } : c))
|
||||
);
|
||||
setPayingId(claim.id);
|
||||
|
||||
try {
|
||||
const updated = await portalAPI.markExpenseClaimPaid(claim.id, nextValue);
|
||||
// Sync local state with server response (in case server normalized anything).
|
||||
setClaims((prev) =>
|
||||
prev.map((c) =>
|
||||
c.id === claim.id ? { ...c, isPaid: Boolean(updated?.isPaid) } : c
|
||||
)
|
||||
);
|
||||
} catch (error: any) {
|
||||
// Rollback on error.
|
||||
setClaims((prev) =>
|
||||
prev.map((c) => (c.id === claim.id ? { ...c, isPaid: previousValue } : c))
|
||||
);
|
||||
alert(error?.response?.data?.message || 'تعذر تحديث حالة القبض');
|
||||
} finally {
|
||||
setPayingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6" dir="rtl">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
@@ -149,18 +192,43 @@ export default function ManagedExpenseClaimsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-600">الحالة:</label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="PENDING">قيد المراجعة</option>
|
||||
<option value="APPROVED">مقبول</option>
|
||||
<option value="REJECTED">مرفوض</option>
|
||||
<option value="all">الكل</option>
|
||||
</select>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-600">بحث عن موظف:</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="الاسم أو الرقم الوظيفي"
|
||||
className="w-64 rounded-lg border border-gray-300 px-3 py-2 pe-8 text-sm"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute inset-y-0 end-2 my-auto h-5 w-5 rounded-full text-gray-400 hover:text-gray-700"
|
||||
aria-label="مسح البحث"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-600">الحالة:</label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="PENDING">قيد المراجعة</option>
|
||||
<option value="APPROVED">مقبول</option>
|
||||
<option value="REJECTED">مرفوض</option>
|
||||
<option value="all">الكل</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -365,6 +433,37 @@ export default function ManagedExpenseClaimsPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{claim.status === 'APPROVED' ? (
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-lg border px-3 py-2 text-sm ${
|
||||
claim.isPaid
|
||||
? 'border-emerald-200 bg-emerald-50 text-emerald-800'
|
||||
: 'border-gray-200 bg-gray-50 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`paid-${claim.id}`}
|
||||
checked={Boolean(claim.isPaid)}
|
||||
disabled={!canMarkAsPaid || payingId === claim.id}
|
||||
onChange={(e) => handleTogglePaid(claim, e.target.checked)}
|
||||
className="h-4 w-4 cursor-pointer accent-emerald-600 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`paid-${claim.id}`}
|
||||
className={`font-medium ${canMarkAsPaid ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
>
|
||||
{claim.isPaid ? 'مقبوض - تم الدفع' : 'غير مقبوض'}
|
||||
</label>
|
||||
{payingId === claim.id && (
|
||||
<span className="text-xs text-gray-500">جارٍ الحفظ...</span>
|
||||
)}
|
||||
{!canMarkAsPaid && (
|
||||
<span className="text-xs text-gray-500">(للعرض فقط)</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{claim.status === 'PENDING' ? (
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<button
|
||||
|
||||
@@ -212,16 +212,34 @@ function TenderDetailContent() {
|
||||
}
|
||||
|
||||
const handleTenderFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (!files.length) return
|
||||
|
||||
setSubmitting(true)
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
try {
|
||||
await tendersAPI.uploadTenderAttachment(tenderId, file)
|
||||
toast.success(t('tenders.uploadFile'))
|
||||
fetchTender()
|
||||
} catch (err: any) {
|
||||
toast.error(err.response?.data?.message || 'Upload failed')
|
||||
// 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)
|
||||
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)
|
||||
e.target.value = ''
|
||||
@@ -234,20 +252,35 @@ function TenderDetailContent() {
|
||||
}
|
||||
|
||||
const handleDirectiveFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
const files = Array.from(e.target.files || [])
|
||||
const directiveId = directiveIdForUpload
|
||||
e.target.value = ''
|
||||
setDirectiveIdForUpload(null)
|
||||
|
||||
if (!file || !directiveId) return
|
||||
if (!files.length || !directiveId) return
|
||||
|
||||
setUploadingDirectiveId(directiveId)
|
||||
let successCount = 0
|
||||
|
||||
try {
|
||||
await tendersAPI.uploadDirectiveAttachment(directiveId, file)
|
||||
toast.success(t('tenders.uploadFile'))
|
||||
fetchTender()
|
||||
} catch (err: any) {
|
||||
toast.error(err.response?.data?.message || 'Upload failed')
|
||||
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)
|
||||
}
|
||||
@@ -462,6 +495,7 @@ function TenderDetailContent() {
|
||||
<input
|
||||
type="file"
|
||||
ref={directiveFileInputRef}
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleDirectiveFileUpload}
|
||||
/>
|
||||
@@ -493,6 +527,7 @@ function TenderDetailContent() {
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleTenderFileUpload}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Permission {
|
||||
canDelete?: boolean
|
||||
canExport?: boolean
|
||||
canApprove?: boolean
|
||||
canMarkAsPaid?: boolean
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
@@ -37,7 +38,7 @@ interface AuthContextType {
|
||||
logout: () => void
|
||||
isLoading: boolean
|
||||
isAuthenticated: boolean
|
||||
hasPermission: (module: string, action: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'approve') => boolean
|
||||
hasPermission: (module: string, action: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'approve' | 'mark-as-paid') => boolean
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
@@ -77,6 +78,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
canDelete: wildcard || p.actions?.includes('delete') || false,
|
||||
canExport: wildcard || p.actions?.includes('export') || false,
|
||||
canApprove: wildcard || p.actions?.includes('approve') || false,
|
||||
canMarkAsPaid: wildcard || p.actions?.includes('mark-as-paid') || false,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -148,7 +150,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const hasPermission = (module: string, action: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'approve'): boolean => {
|
||||
const hasPermission = (module: string, action: 'view' | 'create' | 'edit' | 'delete' | 'export' | 'approve' | 'mark-as-paid'): boolean => {
|
||||
if (!user?.role?.permissions) return false
|
||||
|
||||
const permission = user.role.permissions.find(p => p.module.toLowerCase() === module.toLowerCase())
|
||||
@@ -160,7 +162,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
edit: 'canEdit',
|
||||
delete: 'canDelete',
|
||||
export: 'canExport',
|
||||
approve: 'canApprove'
|
||||
approve: 'canApprove',
|
||||
'mark-as-paid': 'canMarkAsPaid'
|
||||
}
|
||||
|
||||
return permission[actionMap[action] as keyof Permission] as boolean
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface ExpenseClaim {
|
||||
approvedAt?: string | null;
|
||||
rejectedReason?: string | null;
|
||||
approvalNote?: string | null;
|
||||
isPaid?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
attachments?: Array<{
|
||||
@@ -316,7 +317,7 @@ export const portalAPI = {
|
||||
proofRef?: string;
|
||||
}>;
|
||||
description?: string;
|
||||
attachment?: File | null;
|
||||
attachments?: File[];
|
||||
}): Promise<ExpenseClaim> => {
|
||||
const formData = new FormData();
|
||||
|
||||
@@ -326,20 +327,23 @@ export const portalAPI = {
|
||||
formData.append('description', data.description);
|
||||
}
|
||||
|
||||
if (data.attachment) {
|
||||
formData.append('attachment', data.attachment);
|
||||
if (data.attachments && data.attachments.length > 0) {
|
||||
for (const file of data.attachments) {
|
||||
formData.append('attachments', file);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await api.post('/hr/portal/expense-claims', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
headers: { 'Content-Type': undefined as any },
|
||||
});
|
||||
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
getManagedExpenseClaims: async (status?: string): Promise<ExpenseClaim[]> => {
|
||||
getManagedExpenseClaims: async (status?: string, search?: string): Promise<ExpenseClaim[]> => {
|
||||
const q = new URLSearchParams()
|
||||
if (status && status !== 'all') q.append('status', status)
|
||||
if (search && search.trim()) q.append('search', search.trim())
|
||||
const response = await api.get(`/hr/portal/managed-expense-claims?${q.toString()}`)
|
||||
return response.data.data || []
|
||||
},
|
||||
@@ -357,6 +361,11 @@ export const portalAPI = {
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
markExpenseClaimPaid: async (id: string, isPaid: boolean): Promise<ExpenseClaim> => {
|
||||
const response = await api.patch(`/hr/portal/managed-expense-claims/${id}/paid`, { isPaid })
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
getAttendance: async (month?: number, year?: number): Promise<Attendance[]> => {
|
||||
const params = new URLSearchParams()
|
||||
if (month) params.append('month', String(month))
|
||||
|
||||
@@ -176,7 +176,7 @@ export const tendersAPI = {
|
||||
formData.append('file', file)
|
||||
if (category) formData.append('category', category)
|
||||
const response = await api.post(`/tenders/${tenderId}/attachments`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
headers: { 'Content-Type': undefined as any },
|
||||
})
|
||||
return response.data.data
|
||||
},
|
||||
@@ -186,7 +186,7 @@ export const tendersAPI = {
|
||||
formData.append('file', file)
|
||||
if (category) formData.append('category', category)
|
||||
const response = await api.post(`/tenders/directives/${directiveId}/attachments`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
headers: { 'Content-Type': undefined as any },
|
||||
})
|
||||
return response.data.data
|
||||
},
|
||||
@@ -209,4 +209,4 @@ export const tendersAPI = {
|
||||
const response = await api.get('/tenders/directive-type-values')
|
||||
return response.data.data
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user