Files
dsi-commander/resources/js/Components/Commandes/LignesCommandeForm.vue

225 lines
12 KiB
Vue

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