fix: persist user documents immediately after FilePond upload
FilePond success only stored files in the temp bucket, so the documents table stayed empty unless a second commit click was used.
This commit is contained in:
@@ -139,6 +139,23 @@ const isPdf = (img) => {
|
||||
return name.includes('pdf') || name.includes('application/pdf');
|
||||
};
|
||||
|
||||
const parseServerFileMeta = (serverId) => {
|
||||
if (!serverId) return { tempFileName: '', originalName: '', mimeType: '' };
|
||||
if (typeof serverId === 'object') {
|
||||
const nested = serverId.data && typeof serverId.data === 'object' ? serverId.data : serverId;
|
||||
return {
|
||||
tempFileName: nested.tempFileName || nested.filename || '',
|
||||
originalName: nested.originalName || nested.fileName || '',
|
||||
mimeType: nested.mimeType || ''
|
||||
};
|
||||
}
|
||||
try {
|
||||
return parseServerFileMeta(JSON.parse(serverId));
|
||||
} catch {
|
||||
return { tempFileName: String(serverId), originalName: '', mimeType: '' };
|
||||
}
|
||||
};
|
||||
|
||||
const serverOptions = computed(() => ({
|
||||
url: API_BASE_URL,
|
||||
process: {
|
||||
@@ -147,17 +164,12 @@ const serverOptions = computed(() => ({
|
||||
Authorization: `Bearer ${authStore.accessToken}`
|
||||
},
|
||||
onload: (response) => {
|
||||
try {
|
||||
const res = typeof response === 'string' ? JSON.parse(response) : response;
|
||||
const fileData = res.data || res;
|
||||
return JSON.stringify({
|
||||
tempFileName: fileData.tempFileName || fileData.filename,
|
||||
originalName: fileData.originalName,
|
||||
mimeType: fileData.mimeType
|
||||
});
|
||||
} catch (e) {
|
||||
return response;
|
||||
}
|
||||
const meta = parseServerFileMeta(response);
|
||||
return JSON.stringify({
|
||||
tempFileName: meta.tempFileName,
|
||||
originalName: meta.originalName,
|
||||
mimeType: meta.mimeType
|
||||
});
|
||||
}
|
||||
},
|
||||
revert: null
|
||||
@@ -179,20 +191,12 @@ const fetchTempSignedUrl = async (tempFileName) => {
|
||||
const handleProcessFile = async (error, file) => {
|
||||
if (error) return;
|
||||
|
||||
let tempFileName = '';
|
||||
let originalName = file.filename;
|
||||
let mimeType = file.fileType || '';
|
||||
const meta = parseServerFileMeta(file.serverId);
|
||||
const tempFileName = meta.tempFileName || '';
|
||||
const originalName = meta.originalName || file.filename;
|
||||
const mimeType = meta.mimeType || file.fileType || '';
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(file.serverId);
|
||||
tempFileName = parsed.tempFileName || '';
|
||||
originalName = parsed.originalName || originalName;
|
||||
mimeType = parsed.mimeType || mimeType;
|
||||
} catch {
|
||||
tempFileName = file.serverId;
|
||||
}
|
||||
|
||||
const signedUrl = await fetchTempSignedUrl(tempFileName);
|
||||
const signedUrl = tempFileName ? await fetchTempSignedUrl(tempFileName) : '';
|
||||
|
||||
const newItem = {
|
||||
name: originalName,
|
||||
@@ -206,10 +210,14 @@ const handleProcessFile = async (error, file) => {
|
||||
height: 1200
|
||||
};
|
||||
|
||||
const updated = [...imagesList.value, newItem];
|
||||
emit('update:modelValue', updated);
|
||||
emit('update:modelValue', [...imagesList.value, newItem]);
|
||||
emit('upload-success', newItem);
|
||||
await nextTick();
|
||||
try {
|
||||
pond.value?.removeFile?.(file.id);
|
||||
} catch {
|
||||
// FilePond may already have dropped the item
|
||||
}
|
||||
reinitLightbox();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<!-- Upload certificates/documents: FilePond → temp, then commit on submit -->
|
||||
<!-- Upload certificates/documents: FilePond → temp, then immediately persist -->
|
||||
<template>
|
||||
<div class="user-files-section flex flex-column gap-4">
|
||||
<div class="surface-card p-3 sm:p-4 border-round border-1 border-color">
|
||||
<div class="flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold m-0">گواهینامهها</h3>
|
||||
<p class="text-muted text-sm m-0 mt-1">آپلود به باکت موقت، سپس ثبت نهایی در باکت certificates</p>
|
||||
<p class="text-muted text-sm m-0 mt-1">پس از آپلود، گواهینامه بهصورت خودکار در پرونده کاربر ثبت میشود</p>
|
||||
</div>
|
||||
<Button
|
||||
v-if="pendingCertificates.length"
|
||||
type="button"
|
||||
label="ثبت گواهینامهها"
|
||||
label="ثبت گواهینامههای باقیمانده"
|
||||
icon="pi pi-check"
|
||||
size="small"
|
||||
:loading="savingCertificates"
|
||||
@@ -25,6 +25,7 @@
|
||||
:allow-multiple="true"
|
||||
upload-endpoint="/files/admin/upload-temp"
|
||||
temp-bucket="temp"
|
||||
@upload-success="onCertificateUploaded"
|
||||
@remove="onCertificateRemove"
|
||||
/>
|
||||
</div>
|
||||
@@ -33,12 +34,12 @@
|
||||
<div class="flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold m-0">اسناد و مدارک</h3>
|
||||
<p class="text-muted text-sm m-0 mt-1">آپلود به باکت موقت، سپس ثبت نهایی در باکت documents (خصوصی)</p>
|
||||
<p class="text-muted text-sm m-0 mt-1">پس از آپلود، سند بهصورت خودکار در پرونده کاربر ثبت میشود</p>
|
||||
</div>
|
||||
<Button
|
||||
v-if="pendingDocuments.length"
|
||||
type="button"
|
||||
label="ثبت اسناد"
|
||||
label="ثبت اسناد باقیمانده"
|
||||
icon="pi pi-check"
|
||||
size="small"
|
||||
severity="help"
|
||||
@@ -47,8 +48,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-if="pendingDocuments.length">
|
||||
<label class="font-semibold text-sm block mb-2">عنوان سند (اختیاری برای همه فایلهای در انتظار)</label>
|
||||
<div class="mb-3">
|
||||
<label class="font-semibold text-sm block mb-2">عنوان سند (اختیاری برای آپلودهای بعدی)</label>
|
||||
<InputText v-model="documentTitle" class="w-full text-sm" placeholder="مثلاً کارت ملی / شناسنامه" />
|
||||
</div>
|
||||
|
||||
@@ -59,6 +60,7 @@
|
||||
:allow-multiple="true"
|
||||
upload-endpoint="/files/admin/upload-temp"
|
||||
temp-bucket="temp"
|
||||
@upload-success="onDocumentUploaded"
|
||||
@remove="onDocumentRemove"
|
||||
/>
|
||||
</div>
|
||||
@@ -92,6 +94,16 @@ const savingDocuments = ref(false);
|
||||
const pendingCertificates = computed(() => certificateItems.value.filter((i) => i.pending && i.tempFileName));
|
||||
const pendingDocuments = computed(() => documentItems.value.filter((i) => i.pending && i.tempFileName));
|
||||
|
||||
const unwrapList = (res) => {
|
||||
const body = res && typeof res === 'object' && !Array.isArray(res) && res.data !== undefined
|
||||
? res.data
|
||||
: res;
|
||||
if (Array.isArray(body)) return body;
|
||||
if (Array.isArray(body?.data)) return body.data;
|
||||
if (Array.isArray(body?.items)) return body.items;
|
||||
return [];
|
||||
};
|
||||
|
||||
const toGalleryItem = (item) => ({
|
||||
id: item._id || item.id,
|
||||
name: item.title || item.fileName || item.originalName || 'فایل',
|
||||
@@ -113,28 +125,84 @@ const loadFiles = async () => {
|
||||
certificateApi.getByUser(props.userId),
|
||||
documentApi.getByUser(props.userId)
|
||||
]);
|
||||
const certs = certsRes.data || certsRes;
|
||||
const docs = docsRes.data || docsRes;
|
||||
certificateItems.value = (Array.isArray(certs) ? certs : []).map(toGalleryItem);
|
||||
documentItems.value = (Array.isArray(docs) ? docs : []).map(toGalleryItem);
|
||||
const pendingCerts = certificateItems.value.filter((i) => i.pending && i.tempFileName);
|
||||
const pendingDocs = documentItems.value.filter((i) => i.pending && i.tempFileName);
|
||||
certificateItems.value = [...unwrapList(certsRes).map(toGalleryItem), ...pendingCerts];
|
||||
documentItems.value = [...unwrapList(docsRes).map(toGalleryItem), ...pendingDocs];
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
};
|
||||
|
||||
const keepPendingItem = (listRef, file) => {
|
||||
const exists = listRef.value.some((i) => i.pending && i.tempFileName === file.tempFileName);
|
||||
if (!exists) {
|
||||
listRef.value = [...listRef.value, { ...file, pending: true }];
|
||||
}
|
||||
};
|
||||
|
||||
const persistCertificate = async (file) => {
|
||||
if (!file?.tempFileName) {
|
||||
throw new Error('آپلود ناقص است؛ نام فایل موقت دریافت نشد');
|
||||
}
|
||||
await certificateApi.create({
|
||||
user: props.userId,
|
||||
tempFileName: file.tempFileName,
|
||||
originalName: file.fileName || file.name,
|
||||
fileName: file.fileName || file.name,
|
||||
mimeType: file.mimeType,
|
||||
title: file.name || 'گواهینامه'
|
||||
});
|
||||
};
|
||||
|
||||
const persistDocument = async (file) => {
|
||||
if (!file?.tempFileName) {
|
||||
throw new Error('آپلود ناقص است؛ نام فایل موقت دریافت نشد');
|
||||
}
|
||||
await documentApi.create({
|
||||
user: props.userId,
|
||||
tempFileName: file.tempFileName,
|
||||
originalName: file.fileName || file.name,
|
||||
fileName: file.fileName || file.name,
|
||||
mimeType: file.mimeType,
|
||||
title: documentTitle.value || file.name || 'سند'
|
||||
});
|
||||
};
|
||||
|
||||
const dropPending = (listRef, tempFileName) => {
|
||||
listRef.value = listRef.value.filter((i) => !(i.pending && i.tempFileName === tempFileName));
|
||||
};
|
||||
|
||||
const onCertificateUploaded = async (file) => {
|
||||
try {
|
||||
await persistCertificate(file);
|
||||
dropPending(certificateItems, file.tempFileName);
|
||||
showSuccess('گواهینامه ثبت شد');
|
||||
await loadFiles();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
keepPendingItem(certificateItems, file);
|
||||
}
|
||||
};
|
||||
|
||||
const onDocumentUploaded = async (file) => {
|
||||
try {
|
||||
await persistDocument(file);
|
||||
dropPending(documentItems, file.tempFileName);
|
||||
showSuccess('سند ثبت شد');
|
||||
await loadFiles();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
keepPendingItem(documentItems, file);
|
||||
}
|
||||
};
|
||||
|
||||
const commitCertificates = async () => {
|
||||
if (!pendingCertificates.value.length) return;
|
||||
savingCertificates.value = true;
|
||||
try {
|
||||
for (const file of pendingCertificates.value) {
|
||||
await certificateApi.create({
|
||||
user: props.userId,
|
||||
tempFileName: file.tempFileName,
|
||||
originalName: file.fileName || file.name,
|
||||
fileName: file.fileName || file.name,
|
||||
mimeType: file.mimeType,
|
||||
title: file.name || 'گواهینامه'
|
||||
});
|
||||
await persistCertificate(file);
|
||||
}
|
||||
showSuccess('گواهینامهها با موفقیت ثبت شدند');
|
||||
await loadFiles();
|
||||
@@ -150,14 +218,7 @@ const commitDocuments = async () => {
|
||||
savingDocuments.value = true;
|
||||
try {
|
||||
for (const file of pendingDocuments.value) {
|
||||
await documentApi.create({
|
||||
user: props.userId,
|
||||
tempFileName: file.tempFileName,
|
||||
originalName: file.fileName || file.name,
|
||||
fileName: file.fileName || file.name,
|
||||
mimeType: file.mimeType,
|
||||
title: documentTitle.value || file.name || 'سند'
|
||||
});
|
||||
await persistDocument(file);
|
||||
}
|
||||
documentTitle.value = '';
|
||||
showSuccess('اسناد با موفقیت ثبت شدند');
|
||||
@@ -169,6 +230,13 @@ const commitDocuments = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const commitPending = async () => {
|
||||
await Promise.all([
|
||||
pendingCertificates.value.length ? commitCertificates() : Promise.resolve(),
|
||||
pendingDocuments.value.length ? commitDocuments() : Promise.resolve()
|
||||
]);
|
||||
};
|
||||
|
||||
const onCertificateRemove = async (item) => {
|
||||
if (!item?.id || item.pending) return;
|
||||
try {
|
||||
@@ -198,5 +266,5 @@ watch(
|
||||
|
||||
onMounted(loadFiles);
|
||||
|
||||
defineExpose({ reload: loadFiles });
|
||||
defineExpose({ reload: loadFiles, commitPending });
|
||||
</script>
|
||||
|
||||
@@ -60,6 +60,12 @@ export const PERMISSIONS = {
|
||||
CERTIFICATES_DELETE: 'certificates:delete',
|
||||
CERTIFICATES_SEARCH: 'certificates:search',
|
||||
|
||||
DOCUMENTS_CREATE: 'documents:create',
|
||||
DOCUMENTS_READ: 'documents:read',
|
||||
DOCUMENTS_UPDATE: 'documents:update',
|
||||
DOCUMENTS_DELETE: 'documents:delete',
|
||||
DOCUMENTS_SEARCH: 'documents:search',
|
||||
|
||||
FILES_UPLOAD: 'files:upload',
|
||||
FILES_READ: 'files:read',
|
||||
FILES_DELETE: 'files:delete',
|
||||
@@ -184,6 +190,18 @@ export const PERMISSION_GROUPS = [
|
||||
{ key: PERMISSIONS.CERTIFICATES_SEARCH, label: 'جستجوی گواهینامهها' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'documents',
|
||||
label: 'اسناد و مدارک',
|
||||
icon: 'pi pi-file',
|
||||
permissions: [
|
||||
{ key: PERMISSIONS.DOCUMENTS_CREATE, label: 'ایجاد سند' },
|
||||
{ key: PERMISSIONS.DOCUMENTS_READ, label: 'مشاهده اسناد' },
|
||||
{ key: PERMISSIONS.DOCUMENTS_UPDATE, label: 'ویرایش سند' },
|
||||
{ key: PERMISSIONS.DOCUMENTS_DELETE, label: 'حذف سند' },
|
||||
{ key: PERMISSIONS.DOCUMENTS_SEARCH, label: 'جستجوی اسناد' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'files',
|
||||
label: 'مدیریت فایلها',
|
||||
|
||||
@@ -41,6 +41,7 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
|
||||
function hasPermission(perm) {
|
||||
if (!perm) return true;
|
||||
if (String(roleName.value).toLowerCase() === 'superadmin') return true;
|
||||
if (permissions.value.includes('*') || permissions.value.includes('admin:all')) return true;
|
||||
return permissions.value.includes(perm);
|
||||
}
|
||||
|
||||
@@ -108,10 +108,6 @@
|
||||
<AdminNotesField v-model="form.adminNotes" />
|
||||
</div>
|
||||
|
||||
<div v-if="isEditMode" class="col-12 mt-2">
|
||||
<UserFilesSection :user-id="String(userId)" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2" v-if="isEditMode">
|
||||
<label class="font-semibold text-sm">{{ $t('auth.username') }}</label>
|
||||
<InputText
|
||||
@@ -140,6 +136,10 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditMode" class="mt-4">
|
||||
<UserFilesSection ref="filesSection" :user-id="String(userId)" />
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="credentialsVisible"
|
||||
modal
|
||||
@@ -189,6 +189,7 @@ const { showSuccess, showError } = useToast();
|
||||
const userId = route.params.id;
|
||||
const isEditMode = computed(() => !!userId);
|
||||
const isSubmitting = ref(false);
|
||||
const filesSection = ref(null);
|
||||
const roles = ref([]);
|
||||
const credentialsVisible = ref(false);
|
||||
const createdCredentials = reactive({ username: '', password: '' });
|
||||
@@ -335,6 +336,9 @@ const handleSubmit = async () => {
|
||||
const payload = buildPayload();
|
||||
if (isEditMode.value) {
|
||||
await userApi.update(userId, payload);
|
||||
if (filesSection.value?.commitPending) {
|
||||
await filesSection.value.commitPending();
|
||||
}
|
||||
showSuccess('اطلاعات کاربر با موفقیت ویرایش شد');
|
||||
router.push('/users');
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user