139 lines
4.1 KiB
Vue
139 lines
4.1 KiB
Vue
<template>
|
|
<div
|
|
class="relative mt-4 rounded-lg border-2 border-dashed transition-colors"
|
|
:class="isDragging ? 'border-blue-400 bg-blue-50' : 'border-neutral-300 hover:border-neutral-400'"
|
|
@dragover.prevent="isDragging = true"
|
|
@dragleave.prevent="isDragging = false"
|
|
@drop.prevent="handleDrop"
|
|
@click="fileInput?.click()"
|
|
>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
multiple
|
|
class="hidden"
|
|
@change="handleFileSelect"
|
|
/>
|
|
|
|
<div class="flex cursor-pointer flex-col items-center gap-2 px-4 py-6 text-center">
|
|
<Icon name="heroicons:cloud-arrow-up" class="h-8 w-8 text-neutral-400" />
|
|
<p class="text-sm text-neutral-500">{{ $t('taskDocuments.dropzone') }}</p>
|
|
</div>
|
|
|
|
<!-- Upload progress -->
|
|
<div v-if="uploads.length" class="space-y-2 border-t border-neutral-200 px-4 py-3">
|
|
<div v-for="upload in uploads" :key="upload.name" class="flex items-center gap-3">
|
|
<div class="min-w-0 flex-1">
|
|
<p class="truncate text-sm text-neutral-700">{{ upload.name }}</p>
|
|
<div class="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-neutral-200">
|
|
<div
|
|
class="h-full rounded-full transition-all"
|
|
:class="[
|
|
upload.error ? 'bg-red-500' : upload.uploading ? 'animate-pulse bg-blue-400' : 'bg-green-500',
|
|
]"
|
|
:style="{ width: upload.uploading ? '70%' : `${upload.progress}%` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Icon
|
|
v-if="upload.error"
|
|
name="heroicons:exclamation-circle"
|
|
class="h-5 w-5 shrink-0 text-red-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useTaskDocumentService } from '~/services/task-documents'
|
|
|
|
const props = defineProps<{
|
|
taskId?: number
|
|
clientTicketId?: number
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
uploaded: []
|
|
}>()
|
|
|
|
const { upload: uploadFile, uploadForTicket } = useTaskDocumentService()
|
|
const toast = useToast()
|
|
const { t } = useI18n()
|
|
|
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
const isDragging = ref(false)
|
|
|
|
type UploadState = {
|
|
name: string
|
|
progress: number
|
|
uploading: boolean
|
|
error: boolean
|
|
}
|
|
|
|
const uploads = ref<UploadState[]>([])
|
|
|
|
function handleDrop(event: DragEvent) {
|
|
isDragging.value = false
|
|
const files = event.dataTransfer?.files
|
|
if (files?.length) {
|
|
processFiles(Array.from(files))
|
|
}
|
|
}
|
|
|
|
function handleFileSelect(event: Event) {
|
|
const input = event.target as HTMLInputElement
|
|
if (input.files?.length) {
|
|
processFiles(Array.from(input.files))
|
|
input.value = ''
|
|
}
|
|
}
|
|
|
|
async function processFiles(files: File[]) {
|
|
const maxSize = 50 * 1024 * 1024
|
|
|
|
for (const file of files) {
|
|
if (file.size > maxSize) {
|
|
toast.error({
|
|
title: 'Erreur',
|
|
message: t('taskDocuments.maxSizeError'),
|
|
})
|
|
continue
|
|
}
|
|
|
|
const state: UploadState = reactive({
|
|
name: file.name,
|
|
progress: 30,
|
|
uploading: true,
|
|
error: false,
|
|
})
|
|
uploads.value.push(state)
|
|
|
|
try {
|
|
if (props.clientTicketId) {
|
|
await uploadForTicket(props.clientTicketId, file)
|
|
} else if (props.taskId) {
|
|
await uploadFile(props.taskId, file)
|
|
}
|
|
state.uploading = false
|
|
state.progress = 100
|
|
} catch {
|
|
state.uploading = false
|
|
state.error = true
|
|
state.progress = 100
|
|
toast.error({
|
|
title: 'Erreur',
|
|
message: t('taskDocuments.uploadError'),
|
|
})
|
|
}
|
|
|
|
emit('uploaded')
|
|
}
|
|
|
|
// Clean up completed uploads after a delay
|
|
setTimeout(() => {
|
|
uploads.value = uploads.value.filter(u => u.error)
|
|
}, 1500)
|
|
}
|
|
</script>
|