235 lines
6.8 KiB
Vue
235 lines
6.8 KiB
Vue
<template>
|
|
<div class="space-y-4">
|
|
<!-- Upload Area -->
|
|
<div>
|
|
<UFileUpload
|
|
v-model="selectedFiles"
|
|
:accept="allowedFileTypes"
|
|
:multiple="true"
|
|
:disabled="isUploading || disabled"
|
|
:label="t('applicationForms.formElements.fileUpload.label')"
|
|
:description="t('applicationForms.formElements.fileUpload.allowedTypes')"
|
|
variant="area"
|
|
layout="list"
|
|
position="inside"
|
|
@change="handleFileSelect"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Upload Progress -->
|
|
<div v-if="isUploading" class="space-y-2">
|
|
<UProgress :value="uploadProgress" />
|
|
<p class="text-sm text-gray-600">
|
|
{{ t('applicationForms.formElements.fileUpload.uploading') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<UAlert
|
|
v-if="errorMessage"
|
|
color="error"
|
|
variant="soft"
|
|
:title="t('applicationForms.formElements.fileUpload.uploadError')"
|
|
:description="errorMessage"
|
|
:close-button="{ icon: 'i-ph-x', color: 'red', variant: 'link' }"
|
|
@close="errorMessage = ''"
|
|
/>
|
|
|
|
<!-- Uploaded Files List -->
|
|
<div v-if="uploadedFiles.length > 0" class="space-y-2">
|
|
<p class="text-sm font-medium">
|
|
{{ t('applicationForms.formElements.fileUpload.uploadedFiles') }}
|
|
</p>
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="file in uploadedFiles"
|
|
:key="file.fileId"
|
|
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
<UIcon :name="getFileIcon(file.mimeType)" class="text-xl flex-shrink-0" />
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium truncate">{{ file.filename }}</p>
|
|
<p class="text-xs text-gray-500">{{ formatFileSize(file.size) }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
<UButton
|
|
v-if="isViewableInBrowser(file.mimeType)"
|
|
icon="i-ph-eye"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
:title="t('applicationForms.formElements.fileUpload.view')"
|
|
@click="viewFile(file.fileId)"
|
|
/>
|
|
<UButton
|
|
icon="i-ph-download"
|
|
color="info"
|
|
variant="ghost"
|
|
size="sm"
|
|
:disabled="isDownloading"
|
|
@click="downloadFile(file.fileId, file.filename)"
|
|
/>
|
|
<UButton
|
|
icon="i-ph-trash"
|
|
color="error"
|
|
variant="ghost"
|
|
size="sm"
|
|
:disabled="disabled || isDeleting"
|
|
@click="deleteFile(file.fileId)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { EmployeeDataCategory, type FormOptionDto, ProcessingPurpose } from '~~/.api-client'
|
|
import { useFile, type UploadedFileMetadata } from '~/composables/file/useFile'
|
|
import { useUserStore } from '~~/stores/useUserStore'
|
|
|
|
const props = defineProps<{
|
|
formOptions: FormOptionDto[]
|
|
disabled?: boolean
|
|
applicationFormId?: string
|
|
formElementReference?: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:formOptions', value: FormOptionDto[]): void
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const {
|
|
uploadFile: uploadFileApi,
|
|
downloadFile: downloadFileApi,
|
|
viewFile: viewFileApi,
|
|
deleteFile: deleteFileApi,
|
|
parseUploadedFiles,
|
|
createFileMetadata,
|
|
getFileIcon,
|
|
formatFileSize,
|
|
isViewableInBrowser
|
|
} = useFile()
|
|
|
|
const isUploading = ref(false)
|
|
const isDownloading = ref(false)
|
|
const isDeleting = ref(false)
|
|
const uploadProgress = ref(0)
|
|
const errorMessage = ref('')
|
|
const selectedFiles = ref<File[] | null>(null)
|
|
|
|
const allowedFileTypes = '.pdf,.docx,.doc,.odt,.jpg,.jpeg,.png,.zip'
|
|
|
|
// Parse uploaded files from formOptions
|
|
const uploadedFiles = computed<UploadedFileMetadata[]>(() => {
|
|
const values = props.formOptions.map((option) => option.value)
|
|
return parseUploadedFiles(values)
|
|
})
|
|
|
|
const userStore = useUserStore()
|
|
const organizationId = computed(() => userStore.selectedOrganization?.id)
|
|
|
|
const handleFileSelect = async () => {
|
|
if (!selectedFiles.value || selectedFiles.value.length === 0) return
|
|
|
|
const files = Array.isArray(selectedFiles.value) ? selectedFiles.value : [selectedFiles.value]
|
|
errorMessage.value = ''
|
|
|
|
for (const file of files) {
|
|
const maxFileSize = 10 * 1024 * 1024 // 10 MB
|
|
if (file.size > maxFileSize) {
|
|
errorMessage.value = t('applicationForms.formElements.fileUpload.fileTooLarge', {
|
|
filename: file.name,
|
|
maxSize: formatFileSize(maxFileSize)
|
|
})
|
|
continue
|
|
}
|
|
|
|
await uploadFile(file)
|
|
}
|
|
|
|
selectedFiles.value = null
|
|
}
|
|
|
|
const uploadFile = async (file: File) => {
|
|
if (!props.formElementReference) {
|
|
errorMessage.value = 'Missing required context: formElementReference'
|
|
return
|
|
}
|
|
|
|
isUploading.value = true
|
|
uploadProgress.value = 0
|
|
|
|
try {
|
|
const response = await uploadFileApi({
|
|
file,
|
|
applicationFormId: props.applicationFormId,
|
|
formElementReference: props.formElementReference,
|
|
organizationId: organizationId.value
|
|
})
|
|
|
|
const metadata = createFileMetadata(response)
|
|
|
|
const newOption: FormOptionDto = {
|
|
value: JSON.stringify(metadata),
|
|
label: props.formElementReference,
|
|
processingPurpose: props.formOptions[0]?.processingPurpose ?? ProcessingPurpose.None,
|
|
employeeDataCategory: props.formOptions[0]?.employeeDataCategory ?? EmployeeDataCategory.None
|
|
}
|
|
|
|
const updatedOptions = [...props.formOptions, newOption]
|
|
emit('update:formOptions', updatedOptions)
|
|
|
|
uploadProgress.value = 100
|
|
} catch (error: unknown) {
|
|
errorMessage.value = error instanceof Error ? error.message : String(error)
|
|
} finally {
|
|
isUploading.value = false
|
|
uploadProgress.value = 0
|
|
}
|
|
}
|
|
|
|
const downloadFile = async (fileId: string, filename: string) => {
|
|
isDownloading.value = true
|
|
try {
|
|
await downloadFileApi(fileId, filename)
|
|
} catch (error: unknown) {
|
|
errorMessage.value = error instanceof Error ? error.message : String(error)
|
|
} finally {
|
|
isDownloading.value = false
|
|
}
|
|
}
|
|
|
|
const viewFile = (fileId: string) => {
|
|
viewFileApi(fileId)
|
|
}
|
|
|
|
const deleteFile = async (fileId: string) => {
|
|
if (!confirm(t('common.confirmDelete'))) return
|
|
|
|
isDeleting.value = true
|
|
try {
|
|
await deleteFileApi(fileId)
|
|
|
|
// Remove from formOptions
|
|
const updatedOptions = props.formOptions.filter((option) => {
|
|
try {
|
|
const metadata = JSON.parse(option.value) as UploadedFileMetadata
|
|
return metadata.fileId !== fileId
|
|
} catch {
|
|
return true
|
|
}
|
|
})
|
|
emit('update:formOptions', updatedOptions)
|
|
} catch (error: unknown) {
|
|
errorMessage.value = error instanceof Error ? error.message : String(error)
|
|
} finally {
|
|
isDeleting.value = false
|
|
}
|
|
}
|
|
</script>
|