Files
Lesstime/frontend/components/task/TaskDocumentUpload.vue
Matthieu 2a0b202d32 feat(absences) : avancement module absences + suppression du portail client
Deux lots regroupés sur la branche feat/absence-management.

Suppression complète du portail client :
- retire ROLE_CLIENT (security.yaml) ; User::getRoles() ajoute toujours ROLE_USER
- supprime l'entité ClientTicket (+ repo, states, relations), User.client et
  User.allowedProjects, NotificationService, ProjectAllowedExtension, le bloc
  ROLE_CLIENT de MailAccessChecker
- front : pages /portal, layout portal, composants client-ticket/,
  AdminClientTicketTab, services/dto/i18n/docs associés
- fixtures : retire les users client-liot / client-acme
- migration Version20260522110000 (drop client_ticket, user_allowed_projects,
  colonnes liées ; task_document.task_id -> NOT NULL)
- tests : retire les cas obsolètes testant le blocage des clients sur le mail

Module gestion des absences (WIP) :
- entités / migrations (Version20260521160000, Version20260522090000)
- pages absences.vue / team-absences.vue, composants frontend/components/absence/
- services front, AccrueLeaveCommand, PublicHolidayController

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:31:31 +02:00

136 lines
4.0 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
}>()
const emit = defineEmits<{
uploaded: []
}>()
const { upload: uploadFile } = 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.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>