Initial commit with contrats and domaines modules
This commit is contained in:
244
resources/js/Components/Commandes/PiecesJointesSection.vue
Normal file
244
resources/js/Components/Commandes/PiecesJointesSection.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useForm, router } from '@inertiajs/vue3'
|
||||
import ConfirmModal from '@/Components/ConfirmModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
commande: Object,
|
||||
})
|
||||
|
||||
// ── Upload form ────────────────────────────────────────────────────────────────
|
||||
const showUploadForm = ref(false)
|
||||
|
||||
const form = useForm({
|
||||
type: '',
|
||||
fichier: null,
|
||||
description: '',
|
||||
})
|
||||
|
||||
const fileInput = ref(null)
|
||||
|
||||
function selectFile(event) {
|
||||
form.fichier = event.target.files[0] ?? null
|
||||
}
|
||||
|
||||
function submitUpload() {
|
||||
form.post(route('pieces-jointes.store', props.commande.id), {
|
||||
forceFormData: true,
|
||||
onSuccess: () => {
|
||||
form.reset()
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
showUploadForm.value = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function cancelUpload() {
|
||||
form.reset()
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
showUploadForm.value = false
|
||||
}
|
||||
|
||||
// ── Delete ─────────────────────────────────────────────────────────────────────
|
||||
const confirmDelete = ref(false)
|
||||
const deletingId = ref(null)
|
||||
|
||||
function askDelete(id) {
|
||||
deletingId.value = id
|
||||
confirmDelete.value = true
|
||||
}
|
||||
|
||||
function doDelete() {
|
||||
router.delete(route('pieces-jointes.destroy', deletingId.value), {
|
||||
preserveScroll: true,
|
||||
onFinish: () => {
|
||||
confirmDelete.value = false
|
||||
deletingId.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
const TYPES_LABELS = {
|
||||
devis: 'Devis',
|
||||
bon_commande: 'Bon de commande',
|
||||
bon_livraison: 'Bon de livraison',
|
||||
facture: 'Facture',
|
||||
autre: 'Autre',
|
||||
}
|
||||
|
||||
const TYPE_COLORS = {
|
||||
devis: 'bg-purple-100 text-purple-700',
|
||||
bon_commande: 'bg-blue-100 text-blue-700',
|
||||
bon_livraison: 'bg-orange-100 text-orange-700',
|
||||
facture: 'bg-green-100 text-green-700',
|
||||
autre: 'bg-gray-100 text-gray-600',
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return '—'
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
}).format(new Date(d))
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return ''
|
||||
if (bytes < 1024) return bytes + ' o'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko'
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' Mo'
|
||||
}
|
||||
|
||||
function fileIcon(mimeType) {
|
||||
if (!mimeType) return '📎'
|
||||
if (mimeType === 'application/pdf') return '📄'
|
||||
if (mimeType.startsWith('image/')) return '🖼️'
|
||||
if (['application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.oasis.opendocument.text'].includes(mimeType)) return '📝'
|
||||
if (['application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'text/csv'].includes(mimeType)) return '📊'
|
||||
if (mimeType === 'application/zip') return '🗜️'
|
||||
return '📎'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ── Section header ──────────────────────────────────────────────────── -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500">Pièces jointes</h2>
|
||||
<button v-if="!showUploadForm"
|
||||
@click="showUploadForm = true"
|
||||
class="flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 transition-colors">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Upload form ─────────────────────────────────────────────────────── -->
|
||||
<div v-if="showUploadForm"
|
||||
class="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-4 space-y-3">
|
||||
<p class="text-xs font-semibold text-blue-800 uppercase tracking-wide">Nouveau document</p>
|
||||
|
||||
<!-- Type -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Type <span class="text-red-500">*</span></label>
|
||||
<select v-model="form.type"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
:class="{ 'border-red-400': form.errors.type }">
|
||||
<option value="">— Sélectionner un type —</option>
|
||||
<option v-for="(label, key) in TYPES_LABELS" :key="key" :value="key">{{ label }}</option>
|
||||
</select>
|
||||
<p v-if="form.errors.type" class="mt-1 text-xs text-red-500">{{ form.errors.type }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Fichier -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Fichier <span class="text-red-500">*</span></label>
|
||||
<input ref="fileInput" type="file"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.doc,.docx,.xls,.xlsx,.odt,.ods,.csv,.zip"
|
||||
@change="selectFile"
|
||||
class="block w-full text-sm text-gray-700
|
||||
file:mr-3 file:rounded-md file:border-0
|
||||
file:bg-blue-600 file:px-3 file:py-1.5
|
||||
file:text-xs file:font-medium file:text-white
|
||||
hover:file:bg-blue-700 file:cursor-pointer"
|
||||
:class="{ 'ring-1 ring-red-400 rounded-lg': form.errors.fichier }" />
|
||||
<p class="mt-1 text-xs text-gray-400">PDF, images, Word, Excel, OpenDocument, CSV, ZIP — 20 Mo max</p>
|
||||
<p v-if="form.errors.fichier" class="mt-1 text-xs text-red-500">{{ form.errors.fichier }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Description (optionnel)</label>
|
||||
<input v-model="form.description" type="text" maxlength="500"
|
||||
placeholder="ex : Devis n°DEV-2026-042 du 01/04/2026"
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" @click="cancelUpload"
|
||||
class="rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-50 transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="button" @click="submitUpload"
|
||||
:disabled="form.processing || !form.type || !form.fichier"
|
||||
class="rounded-lg bg-blue-600 px-4 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<span v-if="form.processing">Envoi…</span>
|
||||
<span v-else>Envoyer</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Liste des pièces jointes ───────────────────────────────────────── -->
|
||||
<div v-if="commande.pieces_jointes && commande.pieces_jointes.length" class="divide-y divide-gray-100">
|
||||
<div v-for="pj in commande.pieces_jointes" :key="pj.id"
|
||||
class="flex items-start gap-3 py-3 first:pt-0 last:pb-0">
|
||||
<!-- Icône fichier -->
|
||||
<span class="text-2xl leading-none mt-0.5 shrink-0">{{ fileIcon(pj.mime_type) }}</span>
|
||||
|
||||
<!-- Infos -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- Badge type -->
|
||||
<span :class="['rounded-full px-2 py-0.5 text-xs font-medium', TYPE_COLORS[pj.type] ?? 'bg-gray-100 text-gray-600']">
|
||||
{{ TYPES_LABELS[pj.type] ?? pj.type }}
|
||||
</span>
|
||||
<!-- Nom du fichier -->
|
||||
<span class="truncate text-sm font-medium text-gray-900">{{ pj.nom_original }}</span>
|
||||
<!-- Taille -->
|
||||
<span class="text-xs text-gray-400">{{ formatSize(pj.taille) }}</span>
|
||||
</div>
|
||||
<!-- Description -->
|
||||
<p v-if="pj.description" class="mt-0.5 text-xs text-gray-500">{{ pj.description }}</p>
|
||||
<!-- Métadonnées -->
|
||||
<p class="mt-0.5 text-xs text-gray-400">
|
||||
Ajouté par <span class="font-medium text-gray-600">{{ pj.user?.name ?? '—' }}</span>
|
||||
le {{ formatDate(pj.created_at) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<!-- Télécharger -->
|
||||
<a :href="route('pieces-jointes.download', pj.id)"
|
||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-blue-600 transition-colors"
|
||||
title="Télécharger">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</a>
|
||||
<!-- Supprimer -->
|
||||
<button @click="askDelete(pj.id)"
|
||||
class="rounded p-1.5 text-gray-400 hover:bg-red-50 hover:text-red-600 transition-colors"
|
||||
title="Supprimer">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── État vide ───────────────────────────────────────────────────────── -->
|
||||
<p v-else-if="!showUploadForm" class="text-sm text-gray-400 italic">
|
||||
Aucun document joint pour l'instant.
|
||||
</p>
|
||||
|
||||
<!-- ── Modal de confirmation suppression ──────────────────────────────── -->
|
||||
<ConfirmModal
|
||||
:show="confirmDelete"
|
||||
title="Supprimer la pièce jointe"
|
||||
message="Cette action est irréversible. Le fichier sera définitivement supprimé du serveur."
|
||||
confirm-label="Supprimer"
|
||||
@confirm="doDelete"
|
||||
@cancel="confirmDelete = false" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user