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,57 @@
<script setup>
import { usePage } from '@inertiajs/vue3'
import StatutBadge from './StatutBadge.vue'
defineProps({
historique: { type: Array, required: true },
})
const page = usePage()
const statuts = page.props.config?.statuts ?? {}
function formatDate(dt) {
if (!dt) return ''
return new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(dt))
}
</script>
<template>
<div class="flow-root">
<ul class="-mb-8">
<li v-for="(entry, idx) in historique" :key="entry.id" class="relative pb-8">
<!-- Vertical line (except last) -->
<span v-if="idx !== historique.length - 1"
class="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true" />
<div class="relative flex items-start space-x-3">
<!-- Dot -->
<div class="relative flex h-8 w-8 items-center justify-center rounded-full bg-white ring-8 ring-white">
<div class="h-3 w-3 rounded-full bg-blue-500 ring-2 ring-white" />
</div>
<!-- Content -->
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2 text-sm">
<span class="font-medium text-gray-900">{{ entry.user?.name ?? 'Système' }}</span>
<template v-if="entry.ancien_statut">
<StatutBadge :statut="entry.ancien_statut" size="sm" />
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</template>
<StatutBadge :statut="entry.nouveau_statut" size="sm" />
</div>
<p v-if="entry.commentaire" class="mt-1 text-sm text-gray-600 italic">
"{{ entry.commentaire }}"
</p>
<time class="mt-0.5 block text-xs text-gray-400">{{ formatDate(entry.created_at) }}</time>
</div>
</div>
</li>
</ul>
<p v-if="!historique.length" class="text-sm text-gray-400 italic text-center py-4">
Aucun historique disponible.
</p>
</div>
</template>

View File

@@ -0,0 +1,224 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: { type: Array, default: () => [] },
categories: { type: Array, default: () => [] },
articles: { type: Array, default: () => [] },
readonly: { type: Boolean, default: false },
showReceived: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const lignes = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
function addLigne() {
emit('update:modelValue', [
...props.modelValue,
{ designation: '', reference: '', quantite: 1, quantite_recue: 0, unite: 'unité', prix_unitaire_ht: null, taux_tva: 20, categorie_id: null, article_id: null, notes: '' }
])
}
function removeLigne(idx) {
const updated = [...props.modelValue]
updated.splice(idx, 1)
emit('update:modelValue', updated)
}
function updateLigne(idx, field, value) {
const updated = [...props.modelValue]
updated[idx] = { ...updated[idx], [field]: value }
emit('update:modelValue', updated)
}
function onArticleSelect(idx, articleId) {
const article = props.articles.find(a => a.id == articleId)
if (!article) return
const updated = [...props.modelValue]
updated[idx] = {
...updated[idx],
article_id: article.id,
designation: article.designation,
reference: article.reference ?? '',
prix_unitaire_ht: article.prix_unitaire_ht,
unite: article.unite,
categorie_id: article.categorie_id,
}
emit('update:modelValue', updated)
}
function montantHT(ligne) {
const q = parseFloat(ligne.quantite) || 0
const p = parseFloat(ligne.prix_unitaire_ht) || 0
return (q * p).toFixed(2)
}
function montantTTC(ligne) {
const ht = parseFloat(montantHT(ligne))
const tva = parseFloat(ligne.taux_tva) || 0
return (ht * (1 + tva / 100)).toFixed(2)
}
const totalHT = computed(() => props.modelValue.reduce((sum, l) => sum + parseFloat(montantHT(l)), 0).toFixed(2))
const totalTTC = computed(() => props.modelValue.reduce((sum, l) => sum + parseFloat(montantTTC(l)), 0).toFixed(2))
function formatCurrency(val) {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(val)
}
</script>
<template>
<div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Désignation</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Catégorie</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Réf.</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-20">Qté</th>
<th v-if="showReceived" class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-20">Reçu</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase w-20">Unité</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">P.U. HT</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-16">TVA %</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase w-28">Total HT</th>
<th v-if="!readonly" class="px-3 py-2 w-10"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
<tr v-for="(ligne, idx) in lignes" :key="idx">
<!-- Désignation -->
<td class="px-3 py-2">
<div v-if="!readonly" class="space-y-1">
<select v-if="articles.length"
class="block w-full rounded border border-gray-200 px-2 py-1 text-xs text-gray-600 focus:border-blue-500 focus:outline-none"
@change="onArticleSelect(idx, $event.target.value)">
<option value=""> Choisir un article </option>
<option v-for="art in articles" :key="art.id" :value="art.id">{{ art.designation }}</option>
</select>
<input :value="ligne.designation" @input="updateLigne(idx, 'designation', $event.target.value)"
type="text" placeholder="Désignation *" required
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
</div>
<span v-else class="font-medium">{{ ligne.designation }}</span>
</td>
<!-- Catégorie -->
<td class="px-3 py-2">
<select v-if="!readonly"
:value="ligne.categorie_id"
@change="updateLigne(idx, 'categorie_id', $event.target.value || null)"
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none">
<option value=""></option>
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.nom }}</option>
</select>
<span v-else class="text-gray-600">{{ categories.find(c => c.id == ligne.categorie_id)?.nom ?? '—' }}</span>
</td>
<!-- Référence -->
<td class="px-3 py-2">
<input v-if="!readonly" :value="ligne.reference"
@input="updateLigne(idx, 'reference', $event.target.value)"
type="text" placeholder="Réf."
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
<span v-else class="text-gray-600">{{ ligne.reference || '—' }}</span>
</td>
<!-- Quantité -->
<td class="px-3 py-2">
<input v-if="!readonly" :value="ligne.quantite"
@input="updateLigne(idx, 'quantite', $event.target.value)"
type="number" min="0.001" step="0.001" required
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm text-right focus:border-blue-500 focus:outline-none" />
<span v-else class="block text-right">{{ ligne.quantite }}</span>
</td>
<!-- Quantité reçue -->
<td v-if="showReceived" class="px-3 py-2">
<input v-if="!readonly" :value="ligne.quantite_recue"
@input="updateLigne(idx, 'quantite_recue', $event.target.value)"
type="number" min="0" :max="ligne.quantite" step="0.001"
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm text-right focus:border-blue-500 focus:outline-none" />
<span v-else class="block text-right">{{ ligne.quantite_recue }}</span>
</td>
<!-- Unité -->
<td class="px-3 py-2">
<input v-if="!readonly" :value="ligne.unite"
@input="updateLigne(idx, 'unite', $event.target.value)"
type="text" placeholder="unité"
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
<span v-else class="text-gray-600">{{ ligne.unite }}</span>
</td>
<!-- Prix HT -->
<td class="px-3 py-2">
<input v-if="!readonly" :value="ligne.prix_unitaire_ht"
@input="updateLigne(idx, 'prix_unitaire_ht', $event.target.value)"
type="number" min="0" step="0.01" placeholder="0.00"
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm text-right focus:border-blue-500 focus:outline-none" />
<span v-else class="block text-right">{{ ligne.prix_unitaire_ht != null ? formatCurrency(ligne.prix_unitaire_ht) : '—' }}</span>
</td>
<!-- TVA -->
<td class="px-3 py-2">
<input v-if="!readonly" :value="ligne.taux_tva"
@input="updateLigne(idx, 'taux_tva', $event.target.value)"
type="number" min="0" max="100" step="0.1"
class="block w-full rounded border border-gray-200 px-2 py-1.5 text-sm text-right focus:border-blue-500 focus:outline-none" />
<span v-else class="block text-right">{{ ligne.taux_tva }}%</span>
</td>
<!-- Montant HT -->
<td class="px-3 py-2 text-right font-medium text-gray-900">
{{ ligne.prix_unitaire_ht != null ? formatCurrency(montantHT(ligne)) : '—' }}
</td>
<!-- Supprimer -->
<td v-if="!readonly" class="px-3 py-2 text-center">
<button type="button" @click="removeLigne(idx)"
class="text-gray-400 hover:text-red-500 transition-colors">
<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>
</td>
</tr>
<tr v-if="!lignes.length">
<td :colspan="readonly ? 8 : 9" class="px-3 py-6 text-center text-sm text-gray-400 italic">
Aucune ligne de commande.
</td>
</tr>
</tbody>
<!-- Totaux -->
<tfoot v-if="lignes.length" class="bg-gray-50 font-medium">
<tr>
<td :colspan="readonly ? 6 : 7" class="px-3 py-2 text-right text-sm text-gray-600">Total HT</td>
<td colspan="2" class="px-3 py-2 text-right text-sm">{{ formatCurrency(totalHT) }}</td>
<td v-if="!readonly"></td>
</tr>
<tr>
<td :colspan="readonly ? 6 : 7" class="px-3 py-2 text-right text-sm text-gray-600">Total TTC</td>
<td colspan="2" class="px-3 py-2 text-right font-bold text-blue-700">{{ formatCurrency(totalTTC) }}</td>
<td v-if="!readonly"></td>
</tr>
</tfoot>
</table>
</div>
<button v-if="!readonly" type="button" @click="addLigne"
class="mt-3 flex items-center gap-2 text-sm font-medium text-blue-600 hover:text-blue-800 transition-colors">
<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="M12 4v16m8-8H4" />
</svg>
Ajouter une ligne
</button>
</div>
</template>

View 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>

View File

@@ -0,0 +1,19 @@
<script setup>
defineProps({
priorite: { type: String, required: true },
})
const labels = { normale: 'Normale', haute: 'Haute', urgente: 'Urgente' }
const colors = {
normale: 'bg-gray-100 text-gray-600',
haute: 'bg-amber-100 text-amber-700',
urgente: 'bg-red-100 text-red-700',
}
</script>
<template>
<span :class="['inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium whitespace-nowrap', colors[priorite] ?? colors.normale]">
<span v-if="priorite === 'urgente'" class="mr-1"></span>
{{ labels[priorite] ?? priorite }}
</span>
</template>

View File

@@ -0,0 +1,40 @@
<script setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
const props = defineProps({
statut: { type: String, required: true },
size: { type: String, default: 'md' },
})
const page = usePage()
const label = computed(() => page.props.config?.statuts?.[props.statut] ?? props.statut)
const colorClasses = {
brouillon: 'bg-gray-100 text-gray-700',
en_attente_validation: 'bg-yellow-100 text-yellow-700',
validee: 'bg-blue-100 text-blue-700',
commandee: 'bg-indigo-100 text-indigo-700',
partiellement_recue: 'bg-orange-100 text-orange-700',
recue_complete: 'bg-green-100 text-green-700',
cloturee: 'bg-slate-100 text-slate-600',
annulee: 'bg-red-100 text-red-700',
}
const sizeClasses = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2.5 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
}
const classes = computed(() => [
'inline-flex items-center rounded-full font-medium whitespace-nowrap',
colorClasses[props.statut] ?? 'bg-gray-100 text-gray-700',
sizeClasses[props.size] ?? sizeClasses.md,
])
</script>
<template>
<span :class="classes">{{ label }}</span>
</template>

View File

@@ -0,0 +1,73 @@
<script setup>
import { ref } from 'vue'
import { useForm, usePage } from '@inertiajs/vue3'
const props = defineProps({
show: Boolean,
commande: Object,
targetStatut: String,
})
const emit = defineEmits(['close'])
const page = usePage()
const form = useForm({ statut: '', commentaire: '' })
function submit() {
form.statut = props.targetStatut
form.patch(route('commandes.transition', props.commande.id), {
onSuccess: () => {
form.reset()
emit('close')
},
})
}
function close() {
form.reset()
emit('close')
}
const statutLabel = (s) => page.props.config?.statuts?.[s] ?? s
</script>
<template>
<Transition enter-from-class="opacity-0" enter-active-class="transition-opacity duration-150"
leave-to-class="opacity-0" leave-active-class="transition-opacity duration-150">
<div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/50" @click="close" />
<div class="relative w-full max-w-md rounded-xl bg-white p-6 shadow-xl mx-4">
<h3 class="text-lg font-semibold text-gray-900">Changer le statut</h3>
<p class="mt-1 text-sm text-gray-600">
Commande <strong>{{ commande?.numero_commande }}</strong>
<strong>{{ statutLabel(targetStatut) }}</strong>
</p>
<form @submit.prevent="submit" class="mt-4 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">
Commentaire
<span v-if="targetStatut === 'annulee'" class="text-red-500">*</span>
</label>
<textarea v-model="form.commentaire" rows="3"
:required="targetStatut === 'annulee'"
placeholder="Optionnel — précisez la raison si nécessaire"
class="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
<p v-if="form.errors.commentaire" class="mt-1 text-xs text-red-600">{{ form.errors.commentaire }}</p>
</div>
<div class="flex justify-end gap-3">
<button type="button" @click="close"
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</button>
<button type="submit" :disabled="form.processing"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50 transition-colors">
{{ form.processing ? 'Mise à jour...' : 'Confirmer' }}
</button>
</div>
</form>
</div>
</div>
</Transition>
</template>