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