Initial commit with contrats and domaines modules

This commit is contained in:
mrKamoo
2026-04-08 18:07:08 +02:00
commit 092a6a0484
191 changed files with 24639 additions and 0 deletions

View File

@@ -0,0 +1,243 @@
<script setup>
import { ref } from 'vue'
import { useForm, router } from '@inertiajs/vue3'
import ConfirmModal from '@/Components/ConfirmModal.vue'
const props = defineProps({
contrat: 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.contrat.store', props.contrat.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 = {
contrat: 'Contrat',
avenant: 'Avenant',
facture: 'Facture',
autre: 'Autre',
}
const TYPE_COLORS = {
contrat: 'bg-indigo-100 text-indigo-700',
avenant: 'bg-teal-100 text-teal-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 : Contrat final signé"
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="contrat.pieces_jointes && contrat.pieces_jointes.length" class="divide-y divide-gray-100">
<div v-for="pj in contrat.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 v-if="$page.props.auth.user?.roles?.some(r => r.name === 'admin') || pj.user_id === $page.props.auth.user.id"
@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>